Layered Architecture (Controllers, Services, Models) in Node.js

Build scalable and maintainable Node.js applications by mastering the layered architecture pattern.

The Controller Layer

Controllers are the entry point for client requests. They handle incoming HTTP requests, parse input (like request body, params, and query strings), and call the appropriate service to handle the business logic. Their main job is to manage the flow of data, not to contain the logic itself.

The Service Layer

The service layer is the heart of your application's logic. It's responsible for executing business rules, performing complex calculations, and coordinating data from multiple models. It is completely independent of the web layer (it doesn't know about `req` or `res` objects), making it reusable and easier to test.

The Model Layer

The model layer is responsible for all communication with the database. It defines the data schema and provides methods for creating, reading, updating, and deleting (CRUD) records. Using an ORM (like Sequelize) or ODM (like Mongoose) abstracts away raw database queries.

Request Flow & Benefits

The request-response cycle follows a clear, one-way data flow:

  1. Route receives the request and calls the Controller.
  2. Controller validates input and calls the Service.
  3. Service executes business logic and uses the Model.
  4. Model retrieves or manipulates data in the database.
  5. The flow reverses: Model returns data to Service, Service to Controller, and Controller sends the final HTTP response.

This separation leads to more maintainable, scalable, and testable code.

Practice Zone

Test your understanding with these interactive exercises.


Interactive Test 1: Layer Sort

Drag the layer names to their correct descriptions.

Arrastra en el orden correspondiente.


Arrastra las opciones:

Controllers
Services
Models

Completa el código:

Handles HTTP requests and responses.______
Contains the core business logic.______
Interacts directly with the database.______
Unlock with Premium

Interactive Test 2: Complete the Code

Rellena los huecos en cada casilla.


// controllers/userController.js
const userService = require('../services/userService');
exports.getUserById = async (req, res) => {
  try {
    const user = await userService.findUserById(req.params.id);
    res.json(user);
  } catch (error) {
    res.status(500).send('');
  }
};

// services/userService.js
const User = require('../models/userModel');
exports.findUserById = async (id) => {
  return await User.findById(id);
};

// models/userModel.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  role: { type: String, default: '' },
});
module.exports = mongoose.model('User', userSchema);
Unlock with Premium

Knowledge Check

What is the main responsibility of the Services layer?


Unlock with Premium

Architecture in Practice

Structuring your project directory is the first step to implementing this architecture.


1. Project Folder Structure

A clean folder structure mirrors the architecture, making it intuitive to find code.


/src
├── controllers/
│   └── user.controller.js
├── services/
│   └── user.service.js
├── models/
│   └── user.model.js
├── routes/
│   └── user.routes.js
└── app.js
                  

2. Dependency Injection

For even better decoupling and testability, services and models can be "injected" into controllers and services respectively, rather than being hard-coded with `require`. This is an advanced technique often used with libraries like `awilix` or by manually passing instances.

// Instead of require('../services/userService')...
// You might see this in a controller's setup:

module.exports = ({ userService }) => ({
  async createUser(req, res) {
    // Now userService is passed in, not required
    const newUser = await userService.createUser(req.body);
    res.status(201).json(newUser);
  }
});

Practical Takeaway: Start with a clear folder structure. As your application grows, consider dependency injection to make your components more modular and easier to test in isolation.

Architecture Glossary

Separation of Concerns (SoC)
A design principle for separating a computer program into distinct sections. Each section addresses a separate concern, minimizing overlap.
ODM (Object-Document Mapper)
A library that maps between objects in a program and documents in a NoSQL database like MongoDB. Example: Mongoose.
ORM (Object-Relational Mapper)
A library that maps between objects in a program and tables in a relational database (like PostgreSQL). Example: Sequelize.
Business Logic
The high-level rules and processes that are specific to the application's domain. This is the "brain" of the application and resides in the service layer.