Skip to content

Services

intermediate2 min read
  • NestJS
  • Node.js

The layer that holds an application’s business logic, kept separate from the HTTP layer so rules live in one place and can be reused and tested directly.

Prerequisites

  • Controllers and the layered request flow (see Related Topics)

What you'll understand

  • What a service is and what belongs in it
  • Why business logic is separated from the HTTP layer
  • How services make logic reusable and easy to test

Explanation

A service is where an application's business logic lives, the rules about what should happen, independent of how the request arrived. If a controller answers "a POST came in", the service answers "what does it mean to create this user": check the email is unique, hash the password, save the record, send a welcome email. The service owns those decisions.

Keeping this logic in a service, separate from the controller, pays off in two ways. First, reuse: the same createUser logic can be called from an HTTP controller, a command-line script, or a background job, because it does not depend on the request. Second, testability: you can test the rules directly by calling the service, with no HTTP server, no routes, and no mocking of request objects.

Services also become the seam between your logic and the outside world. A service typically depends on other collaborators, a database client, an email sender, another service, rather than creating them itself. Supplying those collaborators from outside (see Related Topics) keeps the service focused on rules and easy to test with substitutes.

Examples

This TypeScript example is a service that owns the create-user rules and depends on collaborators passed in through its constructor:

class UserService {
  constructor(
    private readonly repo: UserRepository,
    private readonly hasher: PasswordHasher,
  ) {}

  async create(input: CreateUserInput) {
    if (await this.repo.findByEmail(input.email)) {
      throw new ConflictError('Email already registered');
    }
    const passwordHash = await this.hasher.hash(input.password);
    return this.repo.create({ email: input.email, passwordHash });
  }
}

Because the logic lives here, a test can call userService.create(...) directly, with a fake repository, and never touch HTTP.

Common mistakes

  • Leaking HTTP concerns (req, res, status codes) into a service.
  • Splitting business rules across controllers and services so no single place is authoritative.
  • Letting a service construct its own dependencies, which makes it hard to test.
  • Turning a service into a thin pass-through that only forwards to the database with no real logic.

Best practices

  • Keep all business rules in services, free of HTTP details.
  • Receive collaborators through the constructor rather than creating them inside.
  • Write unit tests against services directly, using fakes for their dependencies.
  • Give each service a clear, single responsibility.

Further reading

Related topics