Let’s go through MVC, Service Layer, and Repository patterns in Node.js, which are essential for structuring scalable and maintainable applications.
1️⃣ MVC (Model-View-Controller) Pattern
-
Purpose: Separate concerns to make code modular and maintainable.
-
Common in web applications, especially with Express.
Components
Layer | Responsibility |
---|---|
Model | Defines data structure and interacts with the database (e.g., Mongoose) |
View | Presentation layer (optional in APIs; in Node APIs, often JSON responses) |
Controller | Handles incoming requests, calls services, returns responses |
Example (Express + Mongoose)
// models/User.js
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({ name: String, email: String });
module.exports = mongoose.model("User", UserSchema);
// controllers/userController.js
const User = require("../models/User");
exports.getAllUsers = async (req, res) => {
const users = await User.find();
res.json(users);
};
// routes/userRoutes.js
const express = require("express");
const router = express.Router();
const { getAllUsers } = require("../controllers/userController");
router.get("/users", getAllUsers);
module.exports = router;
2️⃣ Service Layer
-
Purpose: Encapsulate business logic separate from controllers and models.
-
Controllers become thin → mainly handle HTTP requests and responses.
-
Makes code testable and reusable.
Example
// services/userService.js
const User = require("../models/User");
exports.getAllUsers = async () => {
// business logic, e.g., filter, sort, caching
return await User.find();
};
// controllers/userController.js
const userService = require("../services/userService");
exports.getAllUsers = async (req, res) => {
const users = await userService.getAllUsers();
res.json(users);
};
-
Benefits:
-
Easier unit testing (services can be tested without HTTP)
-
Reusable business logic
-
3️⃣ Repository Pattern
-
Purpose: Abstract database access behind a repository layer
-
Makes it easy to switch databases or mock data access in tests
-
Focuses solely on data access, not business logic
Example
// repositories/userRepository.js
const User = require("../models/User");
exports.findAll = async () => await User.find();
exports.findById = async (id) => await User.findById(id);
exports.create = async (data) => await User.create(data);
// services/userService.js
const userRepo = require("../repositories/userRepository");
exports.getAllUsers = async () => {
return await userRepo.findAll();
};
-
Benefits:
-
Decouples data access from services
-
Easier to swap DB (MongoDB → PostgreSQL)
-
Improves testability via mocks
-
4️⃣ Layered Architecture Summary
Controller -> handles HTTP, calls Service
Service -> contains business logic, calls Repository
Repository -> data access layer (DB queries)
Model -> database schema/ORM
- Flow Example:
Client → Controller → Service → Repository → Database → Response
5️⃣ Key Takeaways
-
MVC → separates presentation (View), HTTP handling (Controller), and data (Model)
-
Service Layer → centralizes business logic, keeps controllers thin
-
Repository Pattern → abstracts database access for decoupling and testability
-
Benefits of this structure:
-
Modular, maintainable, testable
-
Easier to scale for large applications
-