MVC Architecture: Splitting Routes, Controllers & Service Layers0%
MVC Architecture: Splitting Routes, Controllers & Service Layers
Beginner12 min read•Updated: Jul 11, 2026
MVC Architecture: Splitting Routes, Controllers & Service Layers
In beginner tutorials, developers often stuff database queries, business logic, validation, and responses into a single massive app.get() callback. In production enterprise applications, this leads to an unmaintainable "spaghetti codebase". Professional Node.js engineering adopts the MVC (Model-View-Controller) / Service-Layer Architecture.
1. The Separation of Concerns Pattern
- 1Routes (
routes/): Define endpoints, match HTTP verbs, and apply route-specific middleware (guards). No business logic. - 2Controllers (
controllers/): Parse HTTP inputs (req.body,req.params), call the appropriate service methods, and format the HTTP response. - 3Services (
services/): Pure business logic (tax calculations, calling third-party APIs, orchestrating multi-step workflows). Independent of Expressreq/res. - 4Models (
models/): Database schemas, queries, and data persistence contracts (Mongoose, Prisma, TypeORM).
Visual Architecture & Process Flow
How data and code flow step-by-step
Step 1
HTTP Request -
Step 2
Route -
Step 3
Controller -
Step 4
Service -
Step 5
Model
Database
2. Real-World Implementation
1. The Route: src/routes/product.routes.js
JavaScript
import { Router } from 'express';
import * as productController from '../controllers/product.controller.js';
import { requireAuth } from '../middleware/auth.middleware.js';
const router = Router();
router.get('/', productController.getAllProducts);
router.get('/:id', productController.getProductById);
router.post('/', requireAuth, productController.createProduct);
export default router;
2. The Controller: src/controllers/product.controller.js
JavaScript
import * as productService from '../services/product.service.js';
export async function getAllProducts(req, res, next) {
try {
const { category, page } = req.query;
const products = await productService.getFilteredProducts({ category, page });
res.status(200).json({ status: 'success', data: products });
} catch (error) {
next(error); // Forward to global error handler
}
}
export async function createProduct(req, res, next) {
try {
const product = await productService.createNewProduct(req.body, req.user.id);
res.status(201).json({ status: 'success', data: product });
} catch (error) {
next(error);
}
}
3. The Service: src/services/product.service.js
JavaScript
import Product from '../models/product.model.js';
export async function createNewProduct(payload, creatorId) {
// Business logic: enforce SKU uniqueness, check category limits
const existing = await Product.findOne({ sku: payload.sku });
if (existing) {
const err = new Error('A product with this SKU already exists');
err.statusCode = 409;
throw err;
}
return await Product.create({ ...payload, createdBy: creatorId });
}
Interactive Quiz
Multiple Choice Questions
Question 1
What is the primary responsibility of a Controller in an Express MVC architecture?
Question 2
Why should business logic and database queries NOT be placed directly inside Express route definition files?
Question 3
In the 3-tier architecture (Route -> Controller -> Service), what does the Service layer interact with?
Question 4
How should an error occurring inside a controller be passed to the centralized error-handling middleware?
Question 5
What is the benefit of keeping Service functions free of
req and res objects?Next Lesson
Global Error Handling Middleware & Custom AppError Classes
Continue learning with hands-on practice, examples, and exercises in the upcoming topic.
Related Lessons
Practice Quiz
Test your understanding of this lesson with 5 questions. Each question has one correct answer.