Skip to content

NestJS Services

Services are the most common type of provider in a NestJS application, holding the actual business logic that controllers delegate to. This lesson covers how to design and structure a clean service layer.

What Belongs in a Service?

A service encapsulates a single area of business logic, validating rules, coordinating multiple repositories, calling external APIs, so that controllers stay focused purely on HTTP concerns.

Services are ordinary @Injectable() classes with no special base class required, they can have any constructor, methods, and internal state your feature needs.

import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class CatsService {
  private readonly cats = [{ id: 1, name: 'Whiskers' }];

  findOne(id: number) {
    const cat = this.cats.find((c) => c.id === id);
    if (!cat) {
      throw new NotFoundException('Cat not found');
    }
    return cat;
  }
}

The service, not the controller, decides what counts as "not found" and throws the matching exception. The controller simply calls findOne() and lets Nest's exception layer handle the response.

A Typical Service Method Signature

@Injectable()
export class FeatureService {
  constructor(private readonly repo: SomeRepository) {}

  async doSomething(input: SomeDto): Promise<Result> {
    // validate, coordinate, persist
  }
}
  • Services often inject repositories or other services rather than talking to a database directly.
  • Async methods returning Promise<T> are standard, Nest awaits them automatically.
  • Services should throw framework exceptions (NotFoundException, etc.) for expected error cases.
  • Keep unrelated responsibilities in separate services rather than one large "god" service.

Service Design Cheatsheet

Guidelines for a clean service layer.

Guideline Why
One service per bounded responsibility Keeps classes small and easy to test
Inject repositories, not raw DB clients Keeps persistence details swappable
Throw HTTP exceptions for expected errors Lets Nest's exception layer format responses
Return DTOs or entities, not framework objects Keeps services usable outside HTTP context
Avoid injecting Request/Response directly Keeps services reusable outside HTTP (e.g. CLI, jobs)

Composing Services

Services frequently depend on other services, an OrdersService might inject ProductsService to check stock, and PaymentsService to charge a customer, composing smaller units of logic into a larger workflow.

@Injectable()
export class OrdersService {
  constructor(
    private readonly productsService: ProductsService,
    private readonly paymentsService: PaymentsService,
  ) {}

  async placeOrder(dto: CreateOrderDto) {
    await this.productsService.reserveStock(dto.items);
    return this.paymentsService.charge(dto.paymentMethodId, dto.total);
  }
}

Why This Makes Testing Easier

Because services receive their dependencies through the constructor rather than importing them directly, tests can pass mock objects in place of real dependencies without any special mocking framework tricks.

Common Mistakes

  • Letting one service grow to handle several unrelated responsibilities.
  • Injecting the Express Request object into a service, coupling it tightly to HTTP.
  • Duplicating validation logic across services instead of centralizing it in a DTO or pipe.
  • Returning raw database entities directly instead of mapping them to response DTOs.

Key Takeaways

  • Services hold business logic, keeping controllers thin and focused on HTTP concerns.
  • Services are plain @Injectable() classes with no required base class.
  • Services commonly inject repositories and other services to compose behavior.
  • Constructor-based injection makes services straightforward to test with mocks.

Pro Tip

If a service constructor is accumulating five or more dependencies, treat that as a signal, it often means the service is doing too much and should be split along clearer responsibility lines.