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:
- Route receives the request and calls the Controller.
- Controller validates input and calls the Service.
- Service executes business logic and uses the Model.
- Model retrieves or manipulates data in the database.
- 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:
Completa el código:
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);
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.