MVC Architecture: Splitting Routes, Controllers & Service Layers0%

MVC Architecture: Splitting Routes, Controllers & Service Layers

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

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

  1. 1
    Routes (routes/): Define endpoints, match HTTP verbs, and apply route-specific middleware (guards). No business logic.
  2. 2
    Controllers (controllers/): Parse HTTP inputs (req.body, req.params), call the appropriate service methods, and format the HTTP response.
  3. 3
    Services (services/): Pure business logic (tax calculations, calling third-party APIs, orchestrating multi-step workflows). Independent of Express req/res.
  4. 4
    Models (models/): Database schemas, queries, and data persistence contracts (Mongoose, Prisma, TypeORM).

Visual Architecture & Process Flow

How data and code flow step-by-step

Flowchart
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.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext