Building a Full-Featured Express.js Project Inspired by Laravel's MVC Structure

In this article, we'll explore how to build a full-featured Express.js 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. Express.js, on the other hand, is a minimal and flexible Node.js web application framework that allows you to build web applications and APIs with ease. While Express.js doesn't enforce a strict MVC structure, we can organize our project in a way that mirrors Laravel's structure, making it easier to manage and scale.

We'll start by examining the Laravel project directory structure provided, and then we'll convert it into an Express.js project, ensuring that every feature and component in the Laravel project has an equivalent in the Express.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

Express.js Project Directory Structure

Now, let's convert the Laravel project structure into an Express.js project. We'll organize the Express.js project in a way that mirrors the Laravel structure, ensuring that each component has an equivalent in the Express.js project.

πŸ“‚ express-advanced-project/
│── πŸ“‚ app/
β”‚   │── πŸ“‚ controllers/
β”‚   β”‚   │── πŸ“‚ api/
β”‚   β”‚   β”‚   │── postController.js
β”‚   β”‚   β”‚   │── userController.js
β”‚   β”‚   │── πŸ“‚ web/
β”‚   β”‚   β”‚   │── homeController.js
β”‚   β”‚   β”‚   │── profileController.js
β”‚   │── πŸ“‚ middleware/
β”‚   β”‚   β”‚   │── authenticate.js
β”‚   β”‚   β”‚   │── redirectIfAuthenticated.js
β”‚   │── πŸ“‚ models/
β”‚   β”‚   │── User.js
β”‚   β”‚   │── Post.js
β”‚   β”‚   │── Comment.js
β”‚   │── πŸ“‚ services/
β”‚   β”‚   │── userService.js
β”‚   β”‚   │── postService.js
β”‚   │── πŸ“‚ utils/
β”‚   β”‚   │── apiResponse.js
│── πŸ“‚ 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
│── πŸ“‚ lang/
β”‚   │── πŸ“‚ en/
β”‚   β”‚   │── auth.json
β”‚   β”‚   │── validation.json
│── πŸ“‚ public/
β”‚   │── πŸ“‚ css/
β”‚   β”‚   │── app.css
β”‚   │── πŸ“‚ js/
β”‚   β”‚   │── app.js
β”‚   │── πŸ“‚ images/
β”‚   │── index.html
│── πŸ“‚ routes/
β”‚   │── api.js
β”‚   │── web.js
│── πŸ“‚ storage/
β”‚   │── πŸ“‚ app/
β”‚   β”‚   │── uploads/
β”‚   │── πŸ“‚ logs/
β”‚   β”‚   │── app.log
│── πŸ“‚ tests/
β”‚   │── πŸ“‚ feature/
β”‚   β”‚   │── user.test.js
β”‚   β”‚   │── post.test.js
β”‚   │── πŸ“‚ unit/
β”‚   β”‚   │── userService.test.js
β”‚   β”‚   │── postService.test.js
│── πŸ“‚ views/
β”‚   │── πŸ“‚ layouts/
β”‚   β”‚   │── app.ejs
β”‚   │── πŸ“‚ users/
β”‚   β”‚   │── index.ejs
β”‚   β”‚   │── show.ejs
β”‚   │── πŸ“‚ posts/
β”‚   β”‚   │── index.ejs
β”‚   β”‚   │── show.ejs
│── .env
│── .gitignore
│── package.json
│── README.md
│── server.js

Setting Up the Express.js Project

1. Initialize the Project

First, let's initialize a new Node.js project:

mkdir express-advanced-project
cd express-advanced-project
npm init -y

2. Install Required Dependencies

Next, install the necessary dependencies:

npm install express body-parser mongoose prisma ejs morgan dotenv
  • express: The web framework for Node.js.
  • body-parser: Middleware to parse incoming request bodies.
  • mongoose: MongoDB object modeling tool.
  • prisma: ORM for MySQL/PostgreSQL.
  • ejs: Templating engine for rendering views.
  • morgan: HTTP request logger middleware.
  • dotenv: Load environment variables from a .env file.

3. Create the Basic Server

Create a server.js file in the root directory:

const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const dotenv = require('dotenv');

dotenv.config();

const app = express();
const port = process.env.PORT || 3000;

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(morgan('dev'));

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

4. Organize the Project Structure

Now, let's create the directory structure as outlined above. We'll start by creating the necessary folders and files.

5. Configure Routes

In the routes/ directory, create api.js and web.js files:

routes/api.js:

const express = require('express');
const router = express.Router();

const postController = require('../app/controllers/api/postController');
const userController = require('../app/controllers/api/userController');

router.get('/posts', postController.index);
router.get('/posts/:id', postController.show);
router.post('/posts', postController.store);
router.put('/posts/:id', postController.update);
router.delete('/posts/:id', postController.destroy);

router.get('/users', userController.index);
router.get('/users/:id', userController.show);
router.post('/users', userController.store);
router.put('/users/:id', userController.update);
router.delete('/users/:id', userController.destroy);

module.exports = router;

routes/web.js:

const express = require('express');
const router = express.Router();

const homeController = require('../app/controllers/web/homeController');
const profileController = require('../app/controllers/web/profileController');

router.get('/', homeController.index);
router.get('/profile', profileController.show);

module.exports = router;

6. Create Controllers

In the app/controllers/ directory, create the necessary controller files.

app/controllers/api/postController.js:

const Post = require('../../models/Post');

exports.index = async (req, res) => {
  try {
    const posts = await Post.find();
    res.json(posts);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.show = async (req, res) => {
  try {
    const post = await Post.findById(req.params.id);
    res.json(post);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.store = async (req, res) => {
  try {
    const post = new Post(req.body);
    await post.save();
    res.status(201).json(post);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.update = async (req, res) => {
  try {
    const post = await Post.findByIdAndUpdate(req.params.id, req.body, { new: true });
    res.json(post);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.destroy = async (req, res) => {
  try {
    await Post.findByIdAndDelete(req.params.id);
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

app/controllers/api/userController.js:

const User = require('../../models/User');

exports.index = async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.show = async (req, res) => {
  try {
    const user = await User.findById(req.params.id);
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.store = async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.update = async (req, res) => {
  try {
    const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

exports.destroy = async (req, res) => {
  try {
    await User.findByIdAndDelete(req.params.id);
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

app/controllers/web/homeController.js:

exports.index = (req, res) => {
  res.render('home/index');
};

app/controllers/web/profileController.js:

exports.show = (req, res) => {
  res.render('profile/show');
};

7. Create Models

In the app/models/ directory, create the necessary model files.

app/models/User.js:

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
}, { timestamps: true });

module.exports = mongoose.model('User', userSchema);

app/models/Post.js:

const mongoose = require('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 },
}, { timestamps: true });

module.exports = mongoose.model('Post', postSchema);

app/models/Comment.js:

const mongoose = require('mongoose');

const commentSchema = new mongoose.Schema({
  content: { type: String, required: true },
  post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
}, { timestamps: true });

module.exports = mongoose.model('Comment', commentSchema);

8. Configure Middleware

In the app/middleware/ directory, create the necessary middleware files.

app/middleware/authenticate.js:

const jwt = require('jsonwebtoken');
const User = require('../models/User');

module.exports = async (req, res, next) => {
  try {
    const token = req.header('Authorization').replace('Bearer ', '');
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findOne({ _id: decoded._id, 'tokens.token': token });

    if (!user) {
      throw new Error();
    }

    req.token = token;
    req.user = user;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Please authenticate.' });
  }
};

app/middleware/redirectIfAuthenticated.js:

module.exports = (req, res, next) => {
  if (req.isAuthenticated()) {
    return res.redirect('/');
  }
  next();
};

9. Configure Views

In the views/ directory, create the necessary view files.

views/layouts/app.ejs:

</span>
 lang="en">

   charset="UTF-8">
   name="viewport" content="width=device-width, initial-scale=1.0">
  <%= title %>
   rel="stylesheet" href="/css/app.css">


  <%- include('partials/header') %>
  
    <%- body %>
  
  <%- include('partials/footer') %>





    Enter fullscreen mode
    


    Exit fullscreen mode
    




views/users/index.ejs:

Users

  <% users.forEach(user => { %>
    <%= user.name %>
  <% }) %>




    Enter fullscreen mode
    


    Exit fullscreen mode
    




views/users/show.ejs:

<%= user.name %>
<%= user.email %>



    Enter fullscreen mode
    


    Exit fullscreen mode
    




views/posts/index.ejs:

Posts

  <% posts.forEach(post => { %>
    <%= post.title %>
  <% }) %>




    Enter fullscreen mode
    


    Exit fullscreen mode
    




views/posts/show.ejs:

<%= post.title %>
<%= post.content %>



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  10. Configure Database with Mongoose
In the config/database.js file, configure the MongoDB connection using Mongoose:

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log('MongoDB Connected');
  } catch (error) {
    console.error('MongoDB Connection Error:', error);
    process.exit(1);
  }
};

module.exports = connectDB;



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  11. Configure Database with Prisma
If you prefer to use MySQL or PostgreSQL, you can use Prisma as your ORM. First, initialize Prisma:

npx prisma init



    Enter fullscreen mode
    


    Exit fullscreen mode
    




This will create a prisma/ directory with a schema.prisma file. Configure your database connection in the schema.prisma file:

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

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
    




Next, run the following command to generate the Prisma client:

npx prisma generate



    Enter fullscreen mode
    


    Exit fullscreen mode
    




Finally, create a prismaClient.js file in the config/ directory to initialize the Prisma client:

const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

module.exports = prisma;



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  12. Configure Environment Variables
Create a .env file in the root directory and add the necessary environment variables:

PORT=3000
MONGO_URI=mongodb://localhost:27017/express-advanced-project
DATABASE_URL=mysql://user:password@localhost:3306/express-advanced-project
JWT_SECRET=your_jwt_secret



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  13. Configure Logging
In the config/app.js file, configure logging using morgan:

const morgan = require('morgan');

module.exports = (app) => {
  app.use(morgan('dev'));
};



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  14. Configure Error Handling
In the app/middleware/errorHandler.js file, create a custom error handler:

module.exports = (err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
};



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  15. Configure Testing
In the tests/ directory, create the necessary test files.tests/feature/user.test.js:

const request = require('supertest');
const app = require('../../server');

describe('User API', () => {
  it('should create a new user', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'John Doe', email: 'john@example.com', password: 'password123' });
    expect(res.statusCode).toEqual(201);
    expect(res.body).toHaveProperty('name', 'John Doe');
  });
});



    Enter fullscreen mode
    


    Exit fullscreen mode
    




tests/unit/userService.test.js:

const UserService = require('../../app/services/userService');
const User = require('../../app/models/User');

describe('UserService', () => {
  it('should create a new user', async () => {
    const user = await UserService.createUser({ name: 'John Doe', email: 'john@example.com', password: 'password123' });
    expect(user).toHaveProperty('name', 'John Doe');
  });
});



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  16. Finalize the Project
Finally, update the server.js file to include all the configurations and start the server:

const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const dotenv = require('dotenv');
const connectDB = require('./config/database');
const errorHandler = require('./app/middleware/errorHandler');

dotenv.config();

const app = express();
const port = process.env.PORT || 3000;

// Connect to Database
connectDB();

// Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(morgan('dev'));

// Routes
app.use('/api', require('./routes/api'));
app.use('/', require('./routes/web'));

// Error Handler
app.use(errorHandler);

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  Conclusion
In this article, we've successfully converted a Laravel project structure into an Express.js project, ensuring that each component in the Laravel project has an equivalent in the Express.js project. We've covered how to set up the project, configure routes, create controllers and models, and handle middleware, views, and error handling. We've also explored how to use Mongoose for MongoDB and Prisma for MySQL, providing code examples along the way.By following this structure, you can build a scalable and maintainable Express.js project that mirrors the organization and structure of a Laravel project. This approach allows you to leverage the flexibility of Express.js while maintaining a clean and organized codebase.