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:
<!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>
views/users/index.ejs:
<h1>Users</h1>
<ul>
<% users.forEach(user => { %>
<li><%= user.name %></li>
<% }) %>
</ul>
views/users/show.ejs:
<h1><%= user.name %></h1>
<p><%= user.email %></p>
views/posts/index.ejs:
<h1>Posts</h1>
<ul>
<% posts.forEach(post => { %>
<li><%= post.title %></li>
<% }) %>
</ul>
views/posts/show.ejs:
<h1><%= post.title %></h1>
<p><%= post.content %></p>
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;
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
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
}
Next, run the following command to generate the Prisma client:
npx prisma generate
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;
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
13. Configure Logging
In the config/app.js
file, configure logging using morgan
:
const morgan = require('morgan');
module.exports = (app) => {
app.use(morgan('dev'));
};
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!' });
};
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');
});
});
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');
});
});
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}`);
});
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)