Where should you move business logic out of controllers?
In short: a controller should be thin - it takes input, calls the right "use-case/service", and builds the response. All business logic moves down into lower layers. Here's exactly where:
Where to move the logic
- Services / Use-cases (Application layer)
- What lives here: orchestrating scenarios, transactions, repository calls, integrations, high-level domain checks.
- Controller →
userService.createUser(dto)→ a result/exception. - Named by action:
CreateUser,ChangeEmail,PlaceOrder.
- The Domain layer
- Entities/Value Objects with invariants and business rules (no dependency on Express/the DB).
- Example:
User.changeEmail()enforces the rule "can't change the email more than N times".
- Repositories / DAOs (Data access layer)
- Encapsulate DB/ORM access (Prisma, Sequelize, TypeORM), holding only CRUD and simple queries.
- Interfaces live in the domain, implementations in the infrastructure.
- Schema validation (Validation layer)
- Input/output schemas:
zod/joi/express-validator. - Run before the controller (middleware) or inside a use-case (if it depends on domain rules).
- The controller stays free of if/else trees.
- Authorization/policies (Policy/ACL)
- Access rules:
canUpdateProfile(user, target). - Keep them separate from the controller (policy functions or Casbin/Oso).
- Mappers/DTOs and formatting
- Conversion between layers: request → DTO → domain entity → DTO → response.
- Prevents DB/ORM details from leaking out.
- Cross-cutting concerns go into middleware/infrastructure
- Authentication, logging, tracing, rate limiting, caching, CSRF, CORS, not in the controller.
- Integrations and side effects
- External API clients, email/SMS, queues/jobs (BullMQ/RabbitMQ), cache (Redis), all in the infrastructure layer, called from use-cases.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.