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)