OpenAPI Decorators
NestJS decorators document your API endpoints. tsdevstack uses these OpenAPI specs to generate Kong gateway routes automatically.
Why decorators matter
When you add decorators to your controllers:
- Swagger UI - Interactive API documentation at
/api
- Kong routes - Gateway configuration generated automatically
- Client generation - TypeScript clients can be generated from specs
The gateway routes exactly what the OpenAPI document declares: each path with its methods. An endpoint missing from the document (for example hidden with @ApiExcludeEndpoint()) gets 404 at the gateway. See Gateway Routing.
Two-layer authentication
Authentication is enforced at two independent layers. They look related but serve different purposes and are controlled by different decorators:
- Kong (gateway layer) — decides whether a valid JWT must be present before the request reaches your service. Controlled by
@ApiBearerAuth(). This is about network-level access.
- AuthGuard (backend layer): runs inside NestJS on every request, checks that it came through Kong and identifies the caller. Controlled by
@Public() (which allows anonymous callers). This is about application-level identity.
These two layers exist because Kong handles routing for all services, while AuthGuard runs per-service. They must be configured independently:
Endpoint type reference
Standard NestJS/Swagger Decorators
These are from @nestjs/swagger.
Groups endpoints in Swagger UI:
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('users')
@Controller('users')
export class UsersController {}
@ApiOperation
Describes an endpoint:
import { ApiOperation } from '@nestjs/swagger';
@Post('signup')
@ApiOperation({
operationId: 'signup',
summary: 'Register a new user',
description: 'Creates account and sends verification email'
})
async signup(@Body() dto: SignupDto) {}
The operationId is used for generated client method names.
@ApiResponse
Documents response types:
import { ApiResponse } from '@nestjs/swagger';
@Post('login')
@ApiResponse({ status: 200, description: 'Login successful', type: TokenDto })
@ApiResponse({ status: 401, description: 'Invalid credentials' })
@ApiResponse({ status: 429, description: 'Rate limit exceeded' })
async login(@Body() dto: LoginDto): Promise<TokenDto> {}
@ApiBody
Documents request body:
import { ApiBody } from '@nestjs/swagger';
@Post('login')
@ApiBody({ type: LoginDto, description: 'User credentials' })
async login(@Body() dto: LoginDto) {}
@ApiProperty
Documents DTO properties for the OpenAPI schema:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEmail, IsString, MinLength } from 'class-validator';
export class SignupDto {
@ApiProperty({
example: 'user@example.com',
description: 'User email address'
})
@IsEmail()
email: string;
@ApiProperty({
example: 'securePassword123',
minLength: 8
})
@IsString()
@MinLength(8)
password: string;
@ApiPropertyOptional({
example: 'John Doe'
})
@IsString()
name?: string;
}
DTOs are classes with decorators for both validation (class-validator) and documentation (@nestjs/swagger).
@ApiBearerAuth
Marks endpoints as requiring JWT authentication at the Kong gateway. Routes with this decorator will require a valid JWT token.
import { ApiBearerAuth } from '@nestjs/swagger';
@Controller('user')
@ApiTags('user')
@ApiBearerAuth() // All endpoints require JWT
export class UserController {
@Get('account')
async getAccount(@Req() req: AuthenticatedRequest) {
// req.user contains JWT claims extracted by Kong
}
}
tsdevstack Decorators
Custom decorators from @tsdevstack/nest-common.
@Public
Marks an endpoint so the backend AuthGuard skips user validation. Use together with no @ApiBearerAuth() for fully public endpoints.
import { Public } from '@tsdevstack/nest-common';
@Controller('auth')
@ApiTags('auth')
export class AuthController {
@Post('login')
@Public() // No @ApiBearerAuth = public at Kong, @Public = public at backend
async login(@Body() dto: LoginDto) {}
@Post('signup')
@Public()
async signup(@Body() dto: SignupDto) {}
}
@PartnerApi
Marks endpoints for Partner API access. These routes are exposed under /api/ prefix and require an API key instead of JWT. Only endpoints with @PartnerApi() get a partner route, and a partner key is rejected (403) by any handler without it. Keys are created and managed through the auth-service admin API; see API Keys.
import { PartnerApi } from '@tsdevstack/nest-common';
@Controller('offers')
export class OffersController {
@Get('plans')
@PartnerApi() // Accessible via /api/offers/plans with API key
async getPlans() {}
}
You can combine @ApiBearerAuth() and @PartnerApi() for dual-access endpoints:
@Get('data')
@ApiBearerAuth()
@PartnerApi()
async getData() {
// Accessible via:
// - /service/data with JWT token (for users)
// - /api/service/data with API key (for partners)
}
@Roles
Restricts an endpoint to users holding one of the listed system or custom roles (403 otherwise). It is not an OpenAPI decorator and does not change routing; combine it with @ApiBearerAuth().
import { Roles } from '@tsdevstack/nest-common';
@Get('reports')
@ApiBearerAuth()
@Roles('ADMIN')
async reports() {}
See Roles.
Example: Auth controller (public endpoints)
import { Controller, Post, Body } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import { Public } from '@tsdevstack/nest-common';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
@Post('signup')
@Public() // Public at both Kong and backend
@ApiOperation({ operationId: 'signup', summary: 'Register new user' })
@ApiBody({ type: SignupDto })
@ApiResponse({ status: 201, type: MessageDto })
async signup(@Body() dto: SignupDto): Promise<MessageDto> {}
@Post('login')
@Public()
@ApiOperation({ operationId: 'login', summary: 'Login user' })
@ApiBody({ type: LoginDto })
@ApiResponse({ status: 200, type: TokenDto })
async login(@Body() dto: LoginDto): Promise<TokenDto> {}
}
Example: User controller (protected endpoints)
import { Controller, Get, Req } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import type { AuthenticatedRequest } from '@tsdevstack/nest-common';
@ApiTags('user')
@Controller('user')
@ApiBearerAuth() // All endpoints require JWT
export class UserController {
@Get('account')
@ApiOperation({ operationId: 'getAccount', summary: 'Get user account' })
@ApiResponse({ status: 200, type: UserDto })
async getAccount(@Req() req: AuthenticatedRequest): Promise<UserDto> {
const userId = req.user.id;
// ...
}
}
Viewing your API docs
After starting the dev server, access Swagger UI directly on each service:
http://localhost:<port>/api
For example, if auth-service runs on port 3001: http://localhost:3001/api
Swagger is served directly by each service, not through Kong. See Swagger Docs for details.