Node.js Routes vs Controllers vs Services
Learn the difference between routes, controllers, and services in Node.js. Understand their roles, folder structure, and best practices with simple examples.
Node.js Routes vs Controllers vs Services: A Simple Guide for Clean Project Structure
When building a Node.js application using Express, many developers put all their code inside route files. This works for small projects, but as the application grows, the code becomes difficult to manage.
A better approach is to separate your application into three layers:
- Routes
- Controllers
- Services
This structure makes the code easier to read, test, maintain, and scale.
In this article, you'll learn what each layer does, why it is important, and how they work together.
Why Separate Code into Layers?
Without separation:
router.get("/users/:id", async (req, res) => {
const user = await
User.findById(req.params.id);
if (!user) {
return
res.status(404).json({
message: "User not found",
});
}
res.json(user);
});This route is handling:
- Request handling
- Business logic
- Database operations
- Response formatting
As the application grows, route files become large and difficult to manage.
Instead, separate responsibilities:
Request
↓
Route
↓
Controller
↓
Service
↓
Database
Each layer has one clear job.
What Are Routes?
Routes define application endpoints.
They decide which controller should run when a specific URL is requested.
Responsibilities of Routes
- Define API URLs
- Define HTTP methods
- Connect requests to controllers
- Should not contain business logic
- Should not contain database queries
Example Route
const express = require("express");
const router = express.Router();
const userController = require("../controllers/userController");
router.get("/:id", userController.getUser);
module.exports = router;Here the route only maps:
GET /users/:idto:
userController.getUserNothing more.
What Are Controllers?
Controllers receive requests from routes.
They handle:
- Request data
- Validation
- Calling services
- Sending responses
Controllers act as a bridge between routes and services.
Responsibilities of Controllers
- Read request parameters
- Read request body
- Call services
- Return API responses
- Should not contain complex business logic
- Should not directly access the database
Example Controller
const userService = require("../services/userService");
const getUser = async (req, res) => {
try {
const userId = req.params.id;
const user = await userService.getUserById(userId);
return res.status(200).json(user);
} catch (error) {
return res.status(500).json({
message: error.message,
});
}
};
module.exports = { getUser,};The controller:
- Gets the user ID
- Calls the service
- Sends the response
What Are Services?
Services contain the actual business logic. This is where application rules are implemented.
Examples:
- User registration
- Login
- Payment processing
- Order creation
- Email sending
- Data calculations
Responsibilities of Services
- Business logic
- Database queries
- External API calls
- Data processing
- Should not send HTTP responses
- Should not know about Express request or response objects
Example Service
const User = require("../models/User");
const getUserById = async (id) => {
const user = await User.findById(id);
if (!user) {
throw new Error("User not found");
}
return user;
};
module.exports = { getUserById,};The service focuses only on user-related logic.
It does not know anything about Express.
Complete Flow Example
Let's see how all three layers work together.
Route
router.get("/:id", userController.getUser);Controller
const getUser = async (req, res) => {
const user = await userService.getUserById(
req.params.id
);
res.json(user);
};Service
const getUserById = async (id) => {
return await User.findById(id);
};Request Flow
Client Request
↓
Route
↓
Controller
↓
Service
↓
Database
↓
Service
↓
Controller
↓
Response
Recommended Folder Structure
A common project structure looks like this:
project/
│
├── routes/
│ └── userRoutes.js
│
├── controllers/
│ └── userController.js
│
├── services/
│ └── userService.js
│
├── models/
│ └── User.js
│
├── middlewares/
│
├── config/
│
├── utils/
│
└── app.js
This structure keeps files organized and easy to find.
Example: User Registration
Route
router.post("/register",userController.register);Controller
const register = async (req, res) => {
try {
const user = await userService.registerUser(
req.body
);
return res.status(201).json(user);
} catch (error) {
return res.status(400).json({
message: error.message,
});
}
};Service
const bcrypt = require("bcrypt");
const User = require("../models/User");
const registerUser = async (data) => {
const existingUser =
await User.findOne({ email: data.email,
});
if (existingUser) {
throw new Error("Email already exists");
}
const hashedPassword = await
bcrypt.hash(data.password, 10);
const user = await User.create({...data,password: hashedPassword,
});
return user;
};
module.exports = { registerUser,
};Notice that password hashing and email checks belong in the service because they are business rules.
Benefits of Using Routes, Controllers, and Services
1. Cleaner Code
Each file has a single responsibility.
- Routes handle routes.
- Controllers handle requests.
- Services handle logic.
2. Easier Maintenance
When a bug occurs, you know exactly where to look.
- Route issue: routes
- Response issue: controllers
- Business logic issue: services
3. Better Reusability
Services can be reused in multiple controllers.
Example:
userService.getUserById(id);can be used in:
- Profile APIs
- Admin APIs
- Order APIs
4. Easier Testing
Services can be tested independently.
Example:
await userService.registerUser(data);No Express server is required.
5. Better Team Collaboration
Different developers can work on different layers without conflicts.
- Backend developer: Services
- API developer: Controllers
- Integration developer: Routes
Common Mistakes
Putting DatabaseQueries in Routes
Bad:
router.get("/:id", async (req, res) => {
const user = await User.findByIreq.params.id
);
});Good:
router.get(
"/:id", userController.getUser
);Writing Business Logic in Controllers
Bad:
const hashedPassword = await bcrypt.hash(password, 10);Controllers should not handle business rules.
Move it to services.
Using Response Objects Inside Services
Bad:
res.status(400).json({
message: "User not found",
});Services should return data or throw errors.
Controllers should handle responses.
When Can You Skip Services?
For very small projects:
Route: Controller may be enough.
However, once the application starts growing, adding a service layer is highly recommended.
Most production applications follow:
Route
↓
Controller
↓
Service
↓
Database
because it scales much better.
Best Practices
1. Keep Routes Thin
router.get( "/users/:id", userController.getUser);2. Keep Controllers Simple
const user = await userService.getUserById(id);3. Keep Business Logic in Services
const registerUser = async (data) => {
// business rules
};4. Keep Database Access Organized
Use services or repositories for database operations instead of writing queries everywhere.
Final Thoughts
Routes, controllers, and services each have a specific purpose in a Node.js application.
Layer |
Responsibility |
Routes |
Define API endpoints and connect requests to controllers |
Controllers |
Handle requests and responses |
| Services |
Contain business logic and database operations |
- Routes decide where the request goes.
- Controllers manage the request and response.
- Services perform the actual work.