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.
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.