Skip to main content

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

  1. 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.
  1. 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".
  1. 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.
  1. 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.
  1. Authorization/policies (Policy/ACL)
  • Access rules: canUpdateProfile(user, target).
  • Keep them separate from the controller (policy functions or Casbin/Oso).
  1. Mappers/DTOs and formatting
  • Conversion between layers: request → DTO → domain entity → DTO → response.
  • Prevents DB/ORM details from leaking out.
  1. Cross-cutting concerns go into middleware/infrastructure
  • Authentication, logging, tracing, rate limiting, caching, CSRF, CORS, not in the controller.
  1. 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.