Separation of Concerns: From Theory to Production Code
Separation of Concerns (SoC) isn't just an academic concept; it's a practical strategy that directly impacts your daily development workflow in Node.js. It dictates that a software system should be divided into distinct sections, such that each section addresses a separate concern. In the context of a backend API, these concerns are typically: handling HTTP requests, executing business logic, and managing data persistence.
Why the "Spaghetti Code" Approach Fails
When learning Node.js/Express, it's common to see code like this in a single `server.js` file:
app.post('/register', async (req, res) => {
// 1. Validate Input
if (!req.body.email) return res.status(400).send("Email required");
// 2. Check Database
const existing = await db.query('SELECT * FROM users WHERE email = ?', [req.body.email]);
if (existing) return res.status(400).send("User exists");
// 3. Hash Password (Business Logic)
const hash = await bcrypt.hash(req.body.password, 10);
// 4. Save to DB
await db.query('INSERT INTO users...', [req.body.email, hash]);
// 5. Send Email (More Logic)
await emailClient.sendWelcome(req.body.email);
res.send("Success");
});This route handler is doing too much. It knows about SQL, it knows about email providers, it knows about password hashing, and it knows about HTTP status codes. If you want to change your email provider, you have to edit your HTTP route handler. This makes testing impossible and refactoring a nightmare.
The 3-Layer Architecture Solution
To solve this, we adopt a layered architecture. Each layer has a strict boundary and a specific job.
✔️ The Service Layer (Good)
class UserService {
async register(userData) {
const existing = await userModel.findByEmail(userData.email);
if (existing) throw new Error("User exists");
// ... logic ...
return userModel.create(userData);
}
}Pure logic. No 'req' or 'res'. Easy to test with unit tests.
❌ The Fat Controller (Bad)
exports.register = (req, res) => {
// Logic mixed with HTTP
if(req.body.age < 18) {
// Database calls directly here
db.save(req.body);
}
}Hard to test. Logic is tightly coupled to the web framework.
Benefits of Separation
- Testability: You can test the `UserService` without spinning up an Express server or a real database (by mocking the Model layer).
- Reusability: If you add a CLI command to register admin users later, you can reuse `UserService.register()` without going through an HTTP route.
- Maintainability: Teams can work in parallel. One person optimizes SQL queries in the Models while another adds validation rules in the Services.
Key Takeaway: The Controller should only be a traffic cop. It directs requests to the appropriate service and returns the response. It should never contain "business intelligence" or SQL queries.