Fullstack CourseLearn by building
Back to week 1

Topic

Services

Definition

A Nest service is an @Injectable provider that contains application and data-access logic, kept separate from the HTTP-facing controller so that logic can be reused, tested, and injected independently of any specific route.

In simpler words

A service is where the real work happens — querying the database, applying rules, and shaping the response — while the controller just calls it.

TicketsService is this repo’s clearest example: repository injection, a DTO in, a mapped response out, and NotFoundException for missing records.

After this you can

  • Explain why TicketsService, not TicketsController, injects Repository<Ticket>
  • Read a repo-backed service method (list/get/create/update/remove) and describe each step
  • Explain toResponse() as a boundary between entity and API shape
  • Throw the correct Nest exception for a missing resource

What belongs in a service

Definition

A service holds business rules, data persistence operations, and any validation beyond what a DTO or pipe already checked, so that controllers can stay limited to request/response translation.

In simpler words

If it queries the database, enforces a domain rule, or would need to be reused by more than one route, put it in the service.

TicketsService owns the TypeORM repository, builds queries, maps entities to API responses, and throws domain-appropriate exceptions — none of that lives in TicketsController.

Keeping this logic out of the controller means it is testable in isolation (mock the repository) and reusable if a second controller or a background job ever needs the same operation.

TicketsService shape (apps/api)

@Injectable()
export class TicketsService {
  constructor(
    @InjectRepository(Ticket)
    private readonly ticketsRepo: Repository<Ticket>,
  ) {}

  async get(id: string) {
    const ticket = await this.ticketsRepo.findOne({ where: { id } });
    if (!ticket) throw new NotFoundException('Ticket not found');
    return this.toResponse(ticket);
  }
}

Throwing NotFoundException here means the controller never writes an if-not-found branch.

Shaping responses: entity in, contract shape out

Definition

A private mapping method that converts a persisted entity into the API’s response shape decouples the database column layout from the HTTP contract clients depend on.

In simpler words

toResponse() takes the raw database row and turns it into exactly the JSON fields the frontend expects — so a column rename does not have to become a breaking API change.

Ticket entity fields include DB-specific columns (assignee_id, created_at) while the mapped response uses camelCase (assigneeId, createdAt) for the JSON contract consumed by @repo/contracts and the web app.

list() applies page/limit/status/q filtering with a query builder, then returns { data, meta } — data is the mapped array, meta carries pagination totals the frontend needs for its table UI.

Paginated list contract

async list(query: ListTicketsQueryDto) {
  const qb = this.ticketsRepo.createQueryBuilder('t');
  if (query.status) qb.andWhere('t.status = :status', { status: query.status });
  qb.skip((query.page - 1) * query.limit).take(query.limit);
  const [rows, total] = await qb.getManyAndCount();
  return {
    data: rows.map((r) => this.toResponse(r)),
    meta: { page: query.page, limit: query.limit, total, totalPages: Math.ceil(total / query.limit) },
  };
}

getManyAndCount runs one query pair instead of a separate hand-written count query.

Mistake: leaking the raw entity

// Wrong — exposes DB-shaped fields, couples clients to schema
async get(id: string) {
  return this.ticketsRepo.findOne({ where: { id } }); // raw entity
}

// Right
async get(id: string) {
  const ticket = await this.ticketsRepo.findOne({ where: { id } });
  if (!ticket) throw new NotFoundException('Ticket not found');
  return this.toResponse(ticket);
}

Without mapping, a future column rename becomes an accidental breaking API change.

Keep in mind

  • Constructor-inject the repository via @InjectRepository; never query the database from a controller.
  • Throw NotFoundException / ConflictException / etc. from the service — let the global filter format the error JSON.
  • A service method should be understandable without knowing which controller calls it.

Test

Check your understanding

At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.

Checking your session…

10 questions · concept 3 · syntax 2 · practical 3 · logic 2

Concept1. Which statement best defines Nest services?
Syntax2. Which code-level choice matches Nest services?
Practical3. A reviewer spots a bug related to Nest services. What is the right fix?
Logic4. Which reasoning correctly explains Nest services?
Concept5. Which boundary does correct use of Nest services preserve?
Practical6. What is the safest next step when applying Nest services?
Syntax7. Which implementation direction matches the rule for Nest services?
Logic8. Which consequence follows from applying Nest services correctly?
Concept9. Which claim about Nest services is true in this Nest + Postgres monorepo?
Practical10. Which team practice best demonstrates Nest services?