DEV Community

Harsh Mishra
Harsh Mishra

Posted on

Full-Featured Next.js 14 Project Inspired by Laravel's MVC Structure

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. API Routes (using Next.js App Router)
  2. Models (using Mongoose for MongoDB and Prisma for MySQL)
  3. Services (business logic)
  4. Middleware (authentication, authorization, etc.)
  5. Components (reusable UI components)
  6. Pages (using Next.js App Router)
  7. Events (event-driven architecture)
  8. Utils (utility functions like API responses)
  9. Database Migrations and Seeders
  10. Testing (unit and feature tests)
  11. Environment Configuration
  12. 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 });
  }
}
Enter fullscreen mode Exit fullscreen mode

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 });
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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 } });
};
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

app/components/users/UserList.jsx

export default function UserList({ users }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 });
};
Enter fullscreen mode Exit fullscreen mode

9. Database Migrations and Seeders

Prisma Migrations

Run the following command to create and apply migrations:

npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

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();
  });
Enter fullscreen mode Exit fullscreen mode

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');
  });
});
Enter fullscreen mode Exit fullscreen mode

11. Environment Configuration

.env

DATABASE_URL=mysql://user:password@localhost:3306/nextjs-advanced-project
JWT_SECRET=your_jwt_secret
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)