Separation of Concerns in Node.js

Build robust and maintainable applications by learning how to properly divide your code into logical, single-purpose layers.

Code View
// server.js
app.get('/users/:id', (req, res) => {
  // Database logic...
  // Business logic...
  // Response logic...
});

Let's see a common problem: 'Spaghetti Code'. Here, everything is in one file.

// server.js
app.get('/users/:id', (req, res) => {
  // Database logic...
  // Business logic...
  // Response logic...
});

Routes: The API's Front Door

The Routes layer is the entry point of your API. Its sole responsibility is to define the URL endpoints (e.g., `/users/:id`) and the HTTP methods (GET, POST, etc.) that can access them. It then delegates the incoming request to the appropriate controller function.

Controllers: The Traffic Directors

The Controllers layer acts as the intermediary. It receives the request from the route, extracts necessary information (like parameters, queries, and body), calls the relevant business logic from the service layer, and then formats and sends the final HTTP response (e.g., a JSON object or a status code).

Services: The Application's Brain

The Services layer (or Business Logic Layer) is the brain of your application. It contains the core logic, calculations, data processing, and business rules. It orchestrates calls to the data layer (models) and is completely independent of the web layer (it doesn't know about requests or responses).

Models: The Database Guardians

The Models layer is responsible for all communication with the database. It defines the data schema (the structure of your data) and provides methods to create, read, update, and delete (CRUD) records. This is the only layer that should contain database queries.

Practice Zone


Interactive Test 1: Match the Responsibility

Match the architectural layer to its primary responsibility.

Arrastra en el orden correspondiente.


Arrastra las opciones:

`calculateTotal(order)`
`User.findById(id)`
`router.post("/login", ...)`
`res.status(201).json(user)`

Completa el código:

Defines API Endpoints______
Handles HTTP Request/Response______
Contains Business Logic______
Interacts with the Database______
Unlock with Premium

Interactive Test 2: Complete the Flow

Rellena los huecos en cada casilla.

// controller.js
exports.createUser = async (req, res) => {
  try {
    const newUser = await .createUser(req.body);
    res.status(201).(newUser);
  } catch (error) {
    res.status(500).send("Error");
  }
};

// routes.js
const userController = require('./controller');
router.('/users', userController.createUser);
Unlock with Premium

Practice Example: Write a Controller

Complete the controller function to get a user by ID, call the service, and handle the case where the user is not found.

* Write the code below. Correct characters will be shown in green and incorrect ones in red.

const userService = require('../services/userService'); exports.getUser = async (req, res) => { try { const user = await userService.findUserById(req.params.id); if (!user) { return res.status(404).json({ message: "User not found" }); } res.status(200).json(user); } catch (error) { res.status(500).json({ message: "Server error" }); } };
Unlock with Premium

Knowledge Check

Which architectural layer should be responsible for hashing a user's password before saving it?


Unlock with Premium

SoC: From Theory to Production Code

Separation of Concerns isn't just an academic concept; it's a practical strategy that directly impacts your daily development workflow.


1. Dramatically Improved Testing

When your business logic (Service) is separate from your database logic (Model) and web logic (Controller), you can test each part in isolation. You can write a unit test for a complex pricing calculation in your service layer without needing to spin up a server or connect to a real database. This makes tests faster, more reliable, and easier to write.

2. Seamless Team Collaboration

In a team environment, a well-defined structure is crucial. A frontend developer can work on a new feature knowing exactly which API route to call. A backend developer can focus on optimizing a database query within a model file without worrying about breaking the request/response handling in the controller. This modularity minimizes merge conflicts and allows for parallel development.

3. Future-Proofing and Refactoring

Imagine you need to switch from a REST API to GraphQL. With a good SoC architecture, this task is much simpler. Your core business logic in the service layer remains unchanged. You only need to create new "resolvers" (the GraphQL equivalent of controllers) that call your existing services. The same applies to changing databases; you can swap out MongoDB for PostgreSQL by rewriting the model layer, leaving the rest of the application intact.


Practical Takeaway: Adopting Separation of Concerns is an investment in your project's future. It leads to code that is not only cleaner but also more adaptable, testable, and easier for teams to manage as it grows in complexity.

Architecture Glossary

Route
The part of the application that defines an API endpoint (e.g., `/api/products`). Its only job is to listen for requests at a specific URL and pass them to a controller.
Controller
Acts as a manager for incoming requests. It validates input, calls the necessary business logic from a service, and sends back an appropriate HTTP response to the client.
Service
Contains the core business logic of the application. It is independent of the web framework and handles data manipulation and orchestration before data is saved or sent back.
Model
Represents the structure of the data and is the only layer that communicates directly with the database. It handles all Create, Read, Update, and Delete (CRUD) operations.
Middleware
Functions that have access to the request and response objects. They can execute any code, make changes to the request/response, and end the cycle or pass control to the next middleware. Used for concerns like authentication and logging.