Skip to content

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:

  • @ApiTags groups endpoints in Swagger UI (bet.controller.ts:12).
  • @ApiBearerAuth documents the JWT requirement (bet.controller.ts:13).
  • @UseGuards(JwtGuard) goes on individual methods, not the class — some endpoints may be public (bet.controller.ts:19 vs :32 which has no guard).
  • @Req() req: RequestExt gives access to req.user.id after JWT validation (bet.controller.ts:23,27).
  • Return types are explicit — Promise<PaginatedDto<T>> or Promise<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.
  • UserHttpThrottlerGuard tracks 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.

You're done — test by...

cd ebit-api
npm run start:dev
# Open http://localhost:4000/swagger → find "YourFeature API" section
# Click "Try it out" → authenticate with Bearer token → execute