Building a Full-Featured Next.js 14 Project Inspired by Laravel's MVC Structure
In this article, we'll explore how to build a full-featured Next.js 14 project inspired by Laravel's MVC (Model-View-Controller) structure. Laravel is a powerful PHP framework that follows the MVC pattern, providing a clean and organized way to build web applications. Next.js, on the other hand, is a React framework that enables functionality such as server-side rendering (SSR), static site generation (SSG), and API routes, making it a great choice for building modern web applications.
We'll start by examining the Laravel project directory structure provided, and then we'll convert it into a Next.js 14 project using the App Router, ensuring that every feature and component in the Laravel project has an equivalent in the Next.js project. We'll also cover how to use Mongoose for MongoDB and Prisma for MySQL, providing code examples along the way.
Laravel Project Directory Structure
Here's the Laravel project directory structure provided:
๐ laravel-advanced-project/
โโโ ๐ app/
โ โโโ ๐ Console/
โ โ โโโ Kernel.php
โ โโโ ๐ Events/
โ โ โโโ PostCreated.php
โ โ โโโ UserRegistered.php
โ โโโ ๐ Exceptions/
โ โ โโโ Handler.php
โ โโโ ๐ Http/
โ โ โโโ ๐ Controllers/
โ โ โ โโโ ๐ API/
โ โ โ โ โโโ PostController.php
โ โ โ โ โโโ UserController.php
โ โ โ โโโ ๐ Web/
โ โ โ โ โโโ HomeController.php
โ โ โ โ โโโ ProfileController.php
โ โ โโโ ๐ Middleware/
โ โ โ โโโ Authenticate.php
โ โ โ โโโ RedirectIfAuthenticated.php
โ โ โโโ ๐ Requests/
โ โ โ โโโ UserRequest.php
โ โ โ โโโ PostRequest.php
โ โโโ ๐ Models/
โ โ โโโ User.php
โ โ โโโ Post.php
โ โ โโโ Comment.php
โ โโโ ๐ Notifications/
โ โ โโโ NewCommentNotification.php
โ โโโ ๐ Policies/
โ โ โโโ PostPolicy.php
โ โ โโโ CommentPolicy.php
โ โโโ ๐ Providers/
โ โ โโโ AppServiceProvider.php
โ โ โโโ AuthServiceProvider.php
โ โ โโโ EventServiceProvider.php
โ โโโ ๐ Services/
โ โ โโโ UserService.php
โ โ โโโ PostService.php
โ โโโ ๐ Traits/
โ โ โโโ ApiResponse.php
โโโ ๐ bootstrap/
โ โโโ app.php
โโโ ๐ config/
โ โโโ app.php
โ โโโ auth.php
โ โโโ database.php
โโโ ๐ database/
โ โโโ ๐ factories/
โ โ โโโ UserFactory.php
โ โ โโโ PostFactory.php
โ โโโ ๐ migrations/
โ โ โโโ 2024_01_01_000000_create_users_table.php
โ โ โโโ 2024_01_01_000001_create_posts_table.php
โ โ โโโ 2024_01_01_000002_create_comments_table.php
โ โโโ ๐ seeders/
โ โ โโโ DatabaseSeeder.php
โ โ โโโ UserSeeder.php
โ โ โโโ PostSeeder.php
โโโ ๐ lang/
โ โโโ ๐ en/
โ โ โโโ auth.php
โ โ โโโ validation.php
โโโ ๐ public/
โ โโโ ๐ css/
โ โ โโโ app.css
โ โโโ ๐ js/
โ โ โโโ app.js
โ โโโ ๐ images/
โ โโโ index.php
โโโ ๐ resources/
โ โโโ ๐ views/
โ โ โโโ ๐ layouts/
โ โ โ โโโ app.blade.php
โ โ โโโ ๐ users/
โ โ โ โโโ index.blade.php
โ โ โ โโโ show.blade.php
โ โ โโโ ๐ posts/
โ โ โ โโโ index.blade.php
โ โ โ โโโ show.blade.php
โ โโโ ๐ js/
โ โ โโโ app.js
โ โโโ ๐ sass/
โ โ โโโ app.scss
โโโ ๐ routes/
โ โโโ api.php
โ โโโ web.php
โโโ ๐ storage/
โ โโโ ๐ app/
โ โ โโโ uploads/
โ โโโ ๐ logs/
โ โ โโโ laravel.log
โโโ ๐ tests/
โ โโโ ๐ Feature/
โ โ โโโ UserTest.php
โ โ โโโ PostTest.php
โ โโโ ๐ Unit/
โ โ โโโ UserServiceTest.php
โ โ โโโ PostServiceTest.php
โโโ .env
โโโ .gitignore
โโโ artisan
โโโ composer.json
โโโ package.json
โโโ phpunit.xml
โโโ README.md
โโโ webpack.mix.js
Next.js 14 Project Directory Structure (Using App Router)
Now, let's convert the Laravel project structure into a Next.js 14 project using the App Router. We'll organize the Next.js project in a way that mirrors the Laravel structure, ensuring that each component has an equivalent in the Next.js project.
๐ nextjs-advanced-project/
โโโ ๐ app/
โ โโโ ๐ api/
โ โ โโโ ๐ posts/
โ โ โ โโโ route.js
โ โ โโโ ๐ users/
โ โ โ โโโ route.js
โ โโโ ๐ lib/
โ โ โโโ ๐ events/
โ โ โ โโโ postCreated.js
โ โ โ โโโ userRegistered.js
โ โ โโโ ๐ middleware/
โ โ โ โโโ authenticate.js
โ โ โ โโโ redirectIfAuthenticated.js
โ โ โโโ ๐ models/
โ โ โ โโโ User.js
โ โ โ โโโ Post.js
โ โ โ โโโ Comment.js
โ โ โโโ ๐ services/
โ โ โ โโโ userService.js
โ โ โ โโโ postService.js
โ โ โโโ ๐ utils/
โ โ โ โโโ apiResponse.js
โ โโโ ๐ middleware/
โ โ โโโ authMiddleware.js
โ โโโ ๐ components/
โ โ โโโ ๐ layouts/
โ โ โ โโโ AppLayout.jsx
โ โ โโโ ๐ users/
โ โ โ โโโ UserList.jsx
โ โ โ โโโ UserProfile.jsx
โ โ โโโ ๐ posts/
โ โ โ โโโ PostList.jsx
โ โ โ โโโ PostDetail.jsx
โ โโโ ๐ pages/
โ โ โโโ ๐ users/
โ โ โ โโโ page.jsx
โ โ โ โโโ [id]/
โ โ โ โ โโโ page.jsx
โ โ โโโ ๐ posts/
โ โ โ โโโ page.jsx
โ โ โ โโโ [id]/
โ โ โ โ โโโ page.jsx
โ โ โโโ ๐ profile/
โ โ โ โโโ page.jsx
โ โ โโโ ๐ home/
โ โ โ โโโ page.jsx
โโโ ๐ config/
โ โโโ app.js
โ โโโ auth.js
โ โโโ database.js
โโโ ๐ database/
โ โโโ ๐ migrations/
โ โ โโโ 2024_01_01_000000_create_users_table.js
โ โ โโโ 2024_01_01_000001_create_posts_table.js
โ โ โโโ 2024_01_01_000002_create_comments_table.js
โ โโโ ๐ seeders/
โ โ โโโ databaseSeeder.js
โ โ โโโ userSeeder.js
โ โ โโโ postSeeder.js
โโโ ๐ public/
โ โโโ ๐ css/
โ โ โโโ app.css
โ โโโ ๐ js/
โ โ โโโ app.js
โ โโโ ๐ images/
โโโ ๐ styles/
โ โโโ globals.css
โโโ ๐ tests/
โ โโโ ๐ feature/
โ โ โโโ user.test.js
โ โ โโโ post.test.js
โ โโโ ๐ unit/
โ โ โโโ userService.test.js
โ โ โโโ postService.test.js
โโโ .env
โโโ .gitignore
โโโ package.json
โโโ README.md
โโโ next.config.js
Next.js 14 Project: Complete Implementation
Weโll now go through each directory and file in the Next.js 14 project structure and implement the complete code for everything. This will include:
- API Routes (using Next.js App Router)
- Models (using Mongoose for MongoDB and Prisma for MySQL)
- Services (business logic)
- Middleware (authentication, authorization, etc.)
- Components (reusable UI components)
- Pages (using Next.js App Router)
- Events (event-driven architecture)
- Utils (utility functions like API responses)
- Database Migrations and Seeders
- Testing (unit and feature tests)
- Environment Configuration
- Styling (CSS and global styles)
Letโs start!
1. API Routes
In Next.js 14, API routes are defined in the app/api/
directory. Each route corresponds to a specific resource (e.g., users, posts).
app/api/users/route.js
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// GET /api/users - Fetch all users
export async function GET() {
try {
const users = await prisma.user.findMany();
return NextResponse.json(users);
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 });
}
}
// POST /api/users - Create a new user
export async function POST(request) {
try {
const data = await request.json();
const user = await prisma.user.create({ data });
return NextResponse.json(user, { status: 201 });
} catch (error) {
return NextResponse.json({ error: 'Failed to create user' }, { status: 500 });
}
}
app/api/posts/route.js
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// GET /api/posts - Fetch all posts
export async function GET() {
try {
const posts = await prisma.post.findMany({
include: { author: true, comments: true },
});
return NextResponse.json(posts);
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 });
}
}
// POST /api/posts - Create a new post
export async function POST(request) {
try {
const data = await request.json();
const post = await prisma.post.create({ data });
return NextResponse.json(post, { status: 201 });
} catch (error) {
return NextResponse.json({ error: 'Failed to create post' }, { status: 500 });
}
}
2. Models
Weโll use Prisma for MySQL and Mongoose for MongoDB. Hereโs how to define models for both.
Prisma Models (MySQL)
In the prisma/schema.prisma
file:
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
posts Post[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String
authorId Int
author User @relation(fields: [authorId], references: [id])
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Comment {
id Int @id @default(autoincrement())
content String
postId Int
post Post @relation(fields: [postId], references: [id])
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Mongoose Models (MongoDB)
In the app/lib/models/
directory:
app/lib/models/User.js
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Post' }],
comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }],
}, { timestamps: true });
export default mongoose.models.User || mongoose.model('User', userSchema);
app/lib/models/Post.js
import mongoose from 'mongoose';
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
content: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }],
}, { timestamps: true });
export default mongoose.models.Post || mongoose.model('Post', postSchema);
3. Services
Services contain the business logic for your application. Letโs create services for users and posts.
app/lib/services/userService.js
import prisma from '@/lib/prisma';
export const createUser = async (userData) => {
return await prisma.user.create({ data: userData });
};
export const getUserById = async (userId) => {
return await prisma.user.findUnique({ where: { id: userId } });
};
export const getAllUsers = async () => {
return await prisma.user.findMany();
};
app/lib/services/postService.js
import prisma from '@/lib/prisma';
export const createPost = async (postData) => {
return await prisma.post.create({ data: postData });
};
export const getPostById = async (postId) => {
return await prisma.post.findUnique({ where: { id: postId }, include: { author: true } });
};
export const getAllPosts = async () => {
return await prisma.post.findMany({ include: { author: true } });
};
4. Middleware
Middleware is used for authentication, authorization, and other request/response modifications.
app/middleware/authMiddleware.js
import { NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
export function middleware(request) {
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.redirect('/login');
}
try {
jwt.verify(token, process.env.JWT_SECRET);
return NextResponse.next();
} catch (error) {
return NextResponse.redirect('/login');
}
}
5. Components
Components are reusable UI elements. Letโs create a layout and some user/post components.
app/components/layouts/AppLayout.jsx
export default function AppLayout({ children }) {
return (
<div>
<header>Header</header>
<main>{children}</main>
<footer>Footer</footer>
</div>
);
}
app/components/users/UserList.jsx
export default function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
6. Pages
Pages are defined in the app/pages/
directory. Letโs create pages for users and posts.
app/pages/users/page.jsx
import UserList from '@/components/users/UserList';
import { getAllUsers } from '@/lib/services/userService';
export default async function UsersPage() {
const users = await getAllUsers();
return (
<div>
<h1>Users</h1>
<UserList users={users} />
</div>
);
}
app/pages/posts/page.jsx
import PostList from '@/components/posts/PostList';
import { getAllPosts } from '@/lib/services/postService';
export default async function PostsPage() {
const posts = await getAllPosts();
return (
<div>
<h1>Posts</h1>
<PostList posts={posts} />
</div>
);
}
7. Events
Events are used for event-driven architecture. Letโs create an event for user registration.
app/lib/events/userRegistered.js
import { EventEmitter } from 'events';
class UserRegistered extends EventEmitter {
constructor() {
super();
this.eventName = 'UserRegistered';
}
emitEvent(user) {
this.emit(this.eventName, user);
}
}
export default new UserRegistered();
8. Utils
Utils contain utility functions like API responses.
app/lib/utils/apiResponse.js
export const successResponse = (res, data, status = 200) => {
return res.status(status).json({ success: true, data });
};
export const errorResponse = (res, message, status = 500) => {
return res.status(status).json({ success: false, error: message });
};
9. Database Migrations and Seeders
Prisma Migrations
Run the following command to create and apply migrations:
npx prisma migrate dev --name init
Seeders
In the prisma/seed.js
file:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
await prisma.user.create({
data: {
name: 'John Doe',
email: 'john@example.com',
password: 'password123',
},
});
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
10. Testing
Unit Tests
tests/unit/userService.test.js
import { createUser, getUserById } from '@/lib/services/userService';
describe('UserService', () => {
it('should create a new user', async () => {
const user = await createUser({ name: 'John Doe', email: 'john@example.com', password: 'password123' });
expect(user).toHaveProperty('id');
});
});
11. Environment Configuration
.env
DATABASE_URL=mysql://user:password@localhost:3306/nextjs-advanced-project
JWT_SECRET=your_jwt_secret
12. Styling
styles/globals.css
body {
font-family: Arial, sans-serif;
}
header {
background-color: #333;
color: white;
padding: 1rem;
}
footer {
background-color: #333;
color: white;
padding: 1rem;
text-align: center;
}
Conclusion
This comprehensive guide has covered every aspect of building a Next.js 14 project inspired by Laravelโs MVC structure. Weโve implemented API routes, models, services, middleware, components, pages, events, utils, database migrations, testing, and styling. By following this structure, you can build a scalable and maintainable Next.js application that mirrors the organization of a Laravel project.
Top comments (0)