Chapter 51 โ MVC Architecture (and Service Layer)
๐ Definition
MVC is a separation-of-concerns pattern:
- Model โ data + business rules.
- View โ presentation (in APIs, this becomes JSON shaping).
- Controller โ request handler that orchestrates models and produces a response.
In modern Node apps, a Service layer is added between Controller and Model to keep controllers thin and put business logic in services.
๐ Layered Responsibilities
Request
โ
โผ
[ Route ] โ URL โ controller
โ
โผ
[ Middleware ] โ auth, validation, logging
โ
โผ
[ Controller ] โ parse input, call service, shape response
โ
โผ
[ Service ] โ business logic, transactions, orchestration
โ
โผ
[ Repository / Model ] โ DB access
โ
โผ
[ Database ]๐ป Folder Structure (Express)
src/
โโโ app.js
โโโ server.js
โโโ config/
โ โโโ db.js
โโโ routes/
โ โโโ user.routes.js
โโโ controllers/
โ โโโ user.controller.js
โโโ services/
โ โโโ user.service.js
โโโ models/
โ โโโ user.model.js
โโโ middlewares/
โ โโโ auth.middleware.js
โ โโโ error.middleware.js
โ โโโ validation.middleware.js
โโโ utils/
โ โโโ asyncHandler.js
โ โโโ logger.js
โโโ tests/๐ป Code Example โ Model
// models/user.model.js
import mongoose from "mongoose";
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, select: false },
role: { type: String, enum: ["admin", "user"], default: "user" },
}, { timestamps: true });
export const User = mongoose.model("User", userSchema);๐ป Code Example โ Service
// services/user.service.js
import bcrypt from "bcrypt";
import { User } from "../models/user.model.js";
import { AppError } from "../utils/AppError.js";
export const UserService = {
async create({ email, password }) {
const existing = await User.findOne({ email });
if (existing) throw new AppError(409, "USER_EXISTS", "Email already in use");
const hashed = await bcrypt.hash(password, 10);
return User.create({ email, password: hashed });
},
async findById(id) {
const user = await User.findById(id);
if (!user) throw new AppError(404, "USER_NOT_FOUND", "User not found");
return user;
},
async list({ page = 1, limit = 20 } = {}) {
const [items, total] = await Promise.all([
User.find().skip((page - 1) * limit).limit(limit),
User.countDocuments(),
]);
return { items, total, page, limit };
},
};๐ป Code Example โ Controller
// controllers/user.controller.js
import { UserService } from "../services/user.service.js";
import { asyncHandler } from "../utils/asyncHandler.js";
export const UserController = {
create: asyncHandler(async (req, res) => {
const user = await UserService.create(req.body);
res.status(201).json({ id: user.id, email: user.email });
}),
get: asyncHandler(async (req, res) => {
const user = await UserService.findById(req.params.id);
res.json(user);
}),
list: asyncHandler(async (req, res) => {
const result = await UserService.list(req.query);
res.json(result);
}),
};๐ป Code Example โ Routes
// routes/user.routes.js
import { Router } from "express";
import { UserController } from "../controllers/user.controller.js";
import { authMiddleware } from "../middlewares/auth.middleware.js";
const router = Router();
router.post("/", UserController.create);
router.get("/", authMiddleware, UserController.list);
router.get("/:id", authMiddleware, UserController.get);
export default router;๐ป Code Example โ App Setup
// app.js
import express from "express";
import userRoutes from "./routes/user.routes.js";
import { errorHandler } from "./middlewares/error.middleware.js";
const app = express();
app.use(express.json());
app.use("/api/users", userRoutes);
app.use(errorHandler); // last
export default app;// server.js
import app from "./app.js";
import { connectDB } from "./config/db.js";
const port = process.env.PORT || 3000;
connectDB().then(() => {
app.listen(port, () => console.log(`API on :${port}`));
});๐ป Code Example โ Async Handler Utility
// utils/asyncHandler.js
export const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);๐ป Code Example โ Error Class
// utils/AppError.js
export class AppError extends Error {
constructor(status, code, message, details) {
super(message);
Object.assign(this, { status, code, details });
}
}๐ NestJS Equivalent (Same Pattern, Decorators)
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() dto: CreateUserDto) {
return this.userService.create(dto);
}
@Get(":id")
get(@Param("id") id: string) {
return this.userService.findById(id);
}
}NestJS enforces this layering at the framework level โ see Chapter 38+.
๐ Real-World Impact
- Thin controllers + fat services = easy testing.
- Services contain no HTTP concepts โ reusable from CLI, cron jobs, GraphQL, etc.
- Single source of truth per concern โ maintainable as the team grows.
๐ฏ Likely Interview Questions
- What is MVC?
- Why add a service layer? โ Reusable, testable business logic; HTTP-agnostic.
- What goes in a controller vs a service?
- How would you structure an Express project?