Tutorials Logic, IN info@tutorialslogic.com

Express.js Controllers, Services, and Clean Architecture: Separate Request Code From Business Rules

HTTP and Domain Separation

An Express route becomes easier to maintain when it does not try to do everything itself.

Controllers should care about request and response details. Services should care about the business operation being performed.

This separation helps beginners escape giant route files and helps professionals keep the codebase testable as it grows.

The goal is not to worship architecture diagrams. The goal is to keep responsibilities from collapsing into one place.

What Controllers Should Own

A controller sits near the edge of the system. It reads route parameters, body data, query values, authenticated user context, and then decides how to translate the service result into an HTTP response.

That means controllers should stay close to the web layer. They are not the best home for large business rules, pricing decisions, permission rules, or reusable workflows.

  • Parse request inputs.
  • Call the appropriate service.
  • Translate results into status codes and response bodies.

What Services Are Better At

Services are useful when the same business behavior might be used by more than one route, more than one transport, or more than one future feature. They make reasoning easier because the business action gets a real name instead of hiding inside an endpoint body.

A service can still be small. It does not need to be an elaborate class hierarchy. Sometimes a well-named function that handles one business operation is enough.

  • Keep business decisions out of raw request code.
  • Use services for operations that need to be reused or tested independently.
  • Name services after the action, not the technical layer.

Separate HTTP Code From Business Rules

A route maps a method and path to a handler. The controller reads trusted request data, calls the application operation, and translates the result into an HTTP response. It should not contain complex calculations, database transactions, or third-party integration details. This keeps the HTTP boundary easy to understand and test.

A service or application action expresses the business operation using plain values and explicit dependencies. A repository or data-access module handles persistence queries. Validation protects the HTTP boundary, while the service still enforces business invariants that apply to jobs, commands, and other callers.

Begin with one feature such as creating an order. Define the route, validation middleware, controller, CreateOrder service, and repository methods. Unit-test the service with fake dependencies and integration-test the route with a real test database. Do not add interfaces or layers that have no alternate behavior or testing value.

  • Keep controllers focused on request and response translation.
  • Put reusable business rules in application services.
  • Keep persistence details behind focused data-access functions.
  • Enforce domain invariants below HTTP validation.
  • Add abstractions only when they remove real coupling.

The Clean Architecture Idea Without The Ceremony

People often overcomplicate clean architecture. The practical version is simple: the outer web layer should not own your core business rules, and infrastructure details should not leak everywhere.

If your route handler can be read in a few seconds and your business rule can be tested without spinning up the whole server, you are already moving in a healthy direction.

  • Separate what is web-specific from what is business-specific.
  • Keep data access behind a clear boundary when possible.
  • Avoid ceremony that adds folders but not clarity.

Keep Business Rules Outside Controllers

Model a funds-transfer operation in a service that accepts plain values and dependencies. The controller should translate HTTP input and output but know nothing about transaction mechanics.

If the service throws HTTP-specific response objects or the controller performs balance updates itself, the same rule cannot be reused safely by a job or command-line task.

Verification must use evidence that matches the concept. Call the service directly in unit tests, simulate insufficient funds and repository failure, and assert transaction rollback without constructing req or res. Repeat the check after deliberately introducing the failure, then after the fix. The contrast between those runs is the part that turns a definition into practical understanding.

Transactions, Dependency Direction, And Error Mapping

Dependencies should point toward stable business policy. Services can accept repository, clock, ID generator, and gateway contracts without importing Express request or response types. Infrastructure modules implement those contracts. This makes the operation reusable from a queue worker and prevents framework details from spreading through the codebase.

Define transaction boundaries around the complete invariant. A service may ask a transaction manager to execute repository operations together. Avoid opening a transaction in a controller and then making slow network calls while locks are held. Use an outbox when committed database state must reliably produce an asynchronous event.

Map domain errors at the HTTP edge. Insufficient inventory may become 409, invalid state may become 422, and an absent record may become 404. Unexpected errors go to centralized middleware with a request ID and safe response. Log enough context for diagnosis without exposing tokens, passwords, or private payloads.

  • Keep framework types outside core operations.
  • Place transactions around business invariants.
  • Avoid network calls while database locks are held.
  • Map known domain errors consistently at the edge.
  • Use an outbox for reliable post-commit messaging.

Operation Contract

Define each application operation by typed input, actor context, result, durable effects, and named failures. The controller extracts path, query, body, and authenticated identity into that input; the service applies policy and coordinates repositories; the controller maps the result into HTTP. This shape lets a queue worker or command call the same operation without fabricating request and response objects.

Keep domain failures independent of HTTP status numbers. A NotEnoughInventory failure can become a 409 response today and a failed job classification elsewhere. Infrastructure failures such as connection timeout or unique-constraint violation should be translated only when their meaning is known; an unknown database error remains unexpected rather than being mislabeled as bad user input.

Return operation results rather than mutable infrastructure objects. A create-order service can return an order identifier, accepted state, and version while keeping ORM rows, transactions, and driver metadata private. This prevents controllers from depending on persistence details and gives background callers the same stable result contract.

Transaction Dependency

Repositories participating in one transaction must use the same transaction-scoped database client. Passing only a global pool can accidentally run one write outside the transaction. Provide a unit-of-work callback or explicit transaction context that creates repositories bound to one connection, commits on success, rolls back on failure, and always releases the connection.

Policy Placement

Authentication middleware can establish actor identity, but object permission belongs where the target resource and tenant are known. Scope the repository lookup by actor tenant, then evaluate the action policy before returning sensitive data or changing state. Re-check mutable state inside the transaction when permission depends on ownership or workflow status that another request can change.

Thin controller, named service

This pattern is easier to maintain than a long endpoint body that mixes every concern.

Thin controller, named service
Controller: read request -> call createProjectService -> map success to 201 or failure to 400/403
Service: validate business rules -> persist data -> return result
  • The controller speaks HTTP.
  • The service speaks business behavior.
  • The split becomes especially valuable once more endpoints appear.

Keep Business Rules Outside Controllers example

Keep Business Rules Outside Controllers example
export async function transferFunds(input, accounts, tx) {
  return tx.run(async () => {
    const from = await accounts.lock(input.fromId);
    if (from.balance < input.amount) throw new Error('INSUFFICIENT_FUNDS');
    return accounts.transfer(input);
  });
}

Thin order controller and service

The controller owns HTTP translation while the service owns the operation.

Thin order controller and service
export async function createOrderController(req, res, next) {
  try {
    const order = await createOrder(req.validatedBody, req.user);
    res.status(201).location(`/orders/${order.id}`).json(order);
  } catch (error) {
    next(error);
  }
}

export async function createOrder(input, user) {
  await permissions.assertCanOrder(user, input.customerId);
  return transactions.run(() => orders.createWithLines(input));
}
  • The service does not know about req or res.
  • Authorization is part of the operation.
  • Central middleware maps errors.

Domain error mapping middleware

Translate known failures without leaking internal details.

Domain error mapping middleware
export function errorHandler(error, req, res, next) {
  if (error instanceof OutOfStockError) {
    return res.status(409).json({
      error: { code: 'OUT_OF_STOCK', requestId: req.id }
    });
  }

  req.log.error({ error, requestId: req.id });
  res.status(500).json({
    error: { code: 'INTERNAL_ERROR', requestId: req.id }
  });
}
  • Express error middleware needs four parameters.
  • Do not return stack traces to clients.
  • Keep error codes stable for callers.
Before you move on

Express.js Controllers, Services, and Clean Architecture: Separate Request Code From Business Rules Mastery Check

1 checks
  • Why business logic inside route files becomes painful over time.

Express.js Questions Learners Ask

Not necessarily. Use the simplest form that keeps business behavior reusable and understandable.

When it mixes validation, business decisions, data access, and response mapping in a way that is hard to read or test independently.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.