Add a REST Endpoint¶
Canonical example: BetController at apps/api/src/bet/bet.controller.ts:12-51.
1. Create the controller¶
Add a method to an existing controller, or create a new one.
Follow the BetController pattern:
// apps/api/src/your-feature/your-feature.controller.ts
import { JwtGuard } from '@api/auth/guards';
import { RequestExt } from '@api/types';
import { Controller, Get, Post, Body, Param, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
@ApiTags('YourFeature API')
@ApiBearerAuth()
@Controller('your-feature')
export class YourFeatureController {
constructor(private readonly yourService: YourFeatureService) {}
@UseGuards(JwtGuard)
@Get()
findMany(
@Query() query: FindManyQuery,
@Req() req: RequestExt,
): Promise<PaginatedDto<YourDto>> {
return this.yourService.findMany({ ...query, userId: req.user.id });
}
@UseGuards(JwtGuard)
@Post()
create(
@Body() dto: CreateDto,
@Req() req: RequestExt,
): Promise<YourDto> {
return this.yourService.create(dto, req.user);
}
@Get(':id')
findOne(@Param('id') id: string): Promise<YourDto> {
return this.yourService.findOne(id);
}
}
Key conventions from bet.controller.ts:
@ApiTagsgroups endpoints in Swagger UI (bet.controller.ts:12).@ApiBearerAuthdocuments the JWT requirement (bet.controller.ts:13).@UseGuards(JwtGuard)goes on individual methods, not the class — some endpoints may be public (bet.controller.ts:19vs:32which has no guard).@Req() req: RequestExtgives access toreq.user.idafter JWT validation (bet.controller.ts:23,27).- Return types are explicit —
Promise<PaginatedDto<T>>orPromise<T>.
2. Create the DTO¶
Request DTOs use class-validator; response DTOs use class-transformer.
Follow find-many-bets.dto.ts:24-32:
// apps/api/src/your-feature/dto/your-feature.dto.ts
import { Evo } from '@app/shared/api';
import { createPaginatedQuery } from '@app/shared/api/dto/pagination.dto';
import { Prisma } from '@prisma/client';
import { IsOptional, IsString, MaxLength } from 'class-validator';
// Query DTO — extends the shared paginated query helper
export class FindManyQuery extends createPaginatedQuery({
sortableFields: YourSortBy,
defaultSortBy: YourSortBy.CREATED_AT,
defaultSortOrder: Prisma.SortOrder.desc,
}) {
@IsOptional()
@IsString()
search?: string;
}
// Input DTO
export class CreateDto {
@IsString()
@MaxLength(255)
title: string;
@IsOptional()
@IsString()
description?: string;
}
// Sort enum
export enum YourSortBy {
CREATED_AT = 'CREATED_AT',
}
The createPaginatedQuery helper (libs/shared/src/api/dto/pagination.dto.ts) provides
page, take (capped at 20), sortBy, and sortOrder fields automatically.
3. Create the service¶
Inject the repository via constructor. Follow bet-crud.service.ts:22-24:
// apps/api/src/your-feature/your-feature.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class YourFeatureService {
constructor(private readonly repo: YourFeatureRepository) {}
findMany(params: FindManyQuery & { userId: number }) {
return this.repo.findMany(params);
}
findOne(id: string) {
return this.repo.findOne(id);
}
}
4. Register in the module¶
Follow bet.module.ts:17-37:
// apps/api/src/your-feature/your-feature.module.ts
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [YourFeatureController],
providers: [YourFeatureService, YourFeatureRepository],
exports: [YourFeatureService],
})
export class YourFeatureModule {}
Then import YourFeatureModule in apps/api/src/app.module.ts.
5. Add rate limiting (if needed)¶
For mutation endpoints (bets, auth), add per-user throttling.
Follow roulette.controller.ts:23-35:
import { SkipThrottle, Throttle } from '@nestjs/throttler';
import { UserHttpThrottlerGuard } from 'libs/guards/throttler-per-user.guard';
@SkipThrottle({ profile: true, externalApi: true })
@Controller('your-feature')
export class YourFeatureController {
@Post('action')
@UseGuards(JwtGuard, UserHttpThrottlerGuard)
@Throttle({ default: { limit: 15, ttl: 5000 } })
async action(@Body() dto: ActionDto, @Req() req: RequestExt) {
return this.yourService.action(dto, req.user);
}
}
@SkipThrottle({ profile: true, externalApi: true })on the class exempts named throttle groups.@Throttle({ default: { limit: 15, ttl: 5000 } })on the method: max 15 requests per 5 seconds.UserHttpThrottlerGuardtracks by authenticated user ID, not IP.
6. Swagger annotations¶
Swagger is auto-configured via buildSwagger() in libs/shared/src/common/utils/swagger.utils.ts:7-43.
It runs on every app startup and serves at /swagger (non-production only).
Common decorators:
| Decorator | Purpose | Example |
|---|---|---|
@ApiTags('Name') |
Groups endpoints in Swagger sidebar | bet.controller.ts:12 |
@ApiBearerAuth() |
Shows lock icon, documents JWT | bet.controller.ts:13 |
@ApiPaginatedResponse(Dto) |
Documents paginated response shape | bet.controller.ts:18 |
@ApiOperation({ summary }) |
Endpoint description | Used in auth controllers |
The @ApiPaginatedResponse decorator (libs/shared/src/common/decorators/api-paginated-response.decorator.ts) auto-generates the PaginatedDto<T> schema in the OpenAPI doc.
7. OTel tracing¶
No manual instrumentation needed for controllers. The HTTP instrumentation auto-creates spans
named {METHOD} /route (e.g., GET /bets). Prisma queries get automatic
prisma:client:operation child spans via the tracing preview feature (api.prisma:3).
For service-layer tracing beyond auto-instrumentation, see add-otel-span.md.