Controllers
- NestJS
- Express
- Node.js
The layer that receives an HTTP request, coordinates the work, and returns a response, while delegating the actual business logic to services.
Prerequisites
- Routing and REST basics (see Related Topics)
What you'll understand
- What a controller is responsible for
- Why controllers stay thin and delegate to services
- How separating the HTTP layer from logic keeps code testable
Explanation
A controller is the entry point for a request in a layered application. Its job is narrow: read the incoming request, pull out what it needs (parameters, query, body), call the code that does the real work, and shape the response with the right status code. A controller is about HTTP, not about business rules.
The guiding principle is a thin controller. Business logic, the rules about what should actually happen, belongs in a service (see Related Topics), not in the controller. When a controller stays thin, it is easy to read and the logic underneath can be reused and tested without an HTTP request in the picture. A controller that grows fat with rules becomes hard to test and impossible to reuse.
This separation is a form of separation of concerns. The controller knows about the web, methods, status codes, request shapes, and the service knows about the domain. Frameworks make this explicit: Express uses plain handler functions as controllers, while NestJS provides a Controller class whose methods map to routes. The idea is identical in both.
Examples
This TypeScript example is a thin controller: it validates and reads input, delegates to a service, and returns a status code.
app.post('/users', async (req, res) => {
const input = CreateUser.parse(req.body); // validation at the boundary
const user = await userService.create(input); // business logic lives here
res.status(201).json(user);
});The same responsibility as a NestJS controller method:
@Controller('users')
class UsersController {
constructor(private readonly users: UsersService) {}
@Post()
create(@Body() input: CreateUserDto) {
return this.users.create(input); // delegate to the service
}
}Common mistakes
- Putting business rules or database queries directly in the controller.
- Duplicating the same logic across several controllers instead of sharing a service.
- Returning inconsistent status codes for the same kind of outcome.
- Letting the controller know too much about the database instead of the domain.
Best practices
- Keep controllers thin: parse, delegate, respond.
- Move business logic into services so it can be reused and tested directly.
- Validate input at the controller boundary before delegating (see Related Topics).
- Return accurate HTTP status codes for each outcome.
Further reading
- NestJS, Controllers — https://docs.nestjs.com/controllers