DEV Community

Harsh Mishra
Harsh Mishra

Posted on

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

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

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

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

2. Install Required Dependencies

Next, install the necessary dependencies:

npm install express body-parser mongoose prisma ejs morgan dotenv
Enter fullscreen mode Exit fullscreen mode
  • 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}`);
});
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

app/controllers/web/homeController.js:

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

app/controllers/web/profileController.js:

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

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

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

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

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

app/middleware/redirectIfAuthenticated.js:

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

9. Configure Views

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

views/layouts/app.ejs:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= title %></title>
  <link rel="stylesheet" href="/css/app.css">
</head>
<body>
  <%- include('partials/header') %>
  <main>
    <%- body %>
  </main>
  <%- include('partials/footer') %>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

views/users/index.ejs:

<h1>Users</h1>
<ul>
  <% users.forEach(user => { %>
    <li><%= user.name %></li>
  <% }) %>
</ul>
Enter fullscreen mode Exit fullscreen mode

views/users/show.ejs:

<h1><%= user.name %></h1>
<p><%= user.email %></p>
Enter fullscreen mode Exit fullscreen mode

views/posts/index.ejs:

<h1>Posts</h1>
<ul>
  <% posts.forEach(post => { %>
    <li><%= post.title %></li>
  <% }) %>
</ul>
Enter fullscreen mode Exit fullscreen mode

views/posts/show.ejs:

<h1><%= post.title %></h1>
<p><%= post.content %></p>
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.

Top comments (0)