Controllers & Routes
How tsgonest analyzes NestJS controllers for OpenAPI generation.
tsgonest reads your NestJS controllers and extracts routes, HTTP methods, operation metadata, and tags — all through static analysis of standard NestJS decorators and JSDoc comments.
Controller Prefix
The @Controller() decorator defines the base path for all routes in the controller:
@Controller('users')
export class UsersController {
@Get()
findAll(): UserDto[] { ... } // GET /users
@Get(':id')
findOne(@Param('id') id: string): UserDto { ... } // GET /users/{id}
}Nested paths work as expected:
@Controller('organizations/:orgId/members')
export class MembersController {
@Get()
findAll(@Param('orgId') orgId: string): MemberDto[] { ... }
// GET /organizations/{orgId}/members
}HTTP Method Decorators
All standard NestJS HTTP method decorators are supported:
| Decorator | HTTP Method | Default Status Code |
|---|---|---|
@Get() | GET | 200 |
@Post() | POST | 201 |
@Put() | PUT | 200 |
@Delete() | DELETE | 200 |
@Patch() | PATCH | 200 |
@Head() | HEAD | 200 |
@Options() | OPTIONS | 200 |
@Sse() | GET | 200 |
@EventStream() | GET | 200 |
Each decorator can take an optional sub-path:
@Controller('users')
export class UsersController {
@Get('active')
findActive(): UserDto[] { ... } // GET /users/active
@Post('bulk')
createBulk(@Body() body: CreateUserDto[]): UserDto[] { ... } // POST /users/bulk
}Static analysis limits
Runtime-generated controllers and dynamic decorator path arguments (e.g., @Controller(prefix), @Get(dynamicPath)) are not supported. tsgonest emits a warning and excludes these from OpenAPI output. See Rules & Limitations for full details.
Controller Inheritance
tsgonest follows extends clauses to collect routes from base classes. This lets you define shared CRUD routes in an abstract or concrete base class and inherit them:
abstract class CrudBase<T> {
@Get()
findAll(): T[] { ... }
@Get(':id')
findOne(@Param('id') id: string): T { ... }
@Post()
create(@Body() body: T): T { ... }
}
@Controller('users')
export class UsersController extends CrudBase<UserDto> {
// Inherits GET /users, GET /users/:id, POST /users
// All tagged "User" (from the child class name)
}How it works
- Routes from base classes are included in the child controller's OpenAPI output
- Method overrides win — if the child redefines a method, only the child's version is used
- Tags come from the child — inherited routes use the child controller's class name and
@tag - Multi-level inheritance works —
A extends B extends Ccollects routes from all ancestors implementsis ignored — onlyextendsis followed (interfaces have no method bodies)- Abstract base classes work the same as concrete ones
OpenAPI only
Controller inheritance affects OpenAPI generation only. The rewriter cannot inject validation into inherited methods because they only exist on Base.prototype in the emitted JS. If you need validation on inherited route handlers, override them in the child class.
Operation IDs
The method name becomes the operationId in the OpenAPI document:
@Controller('users')
export class UsersController {
@Get()
findAll(): UserDto[] { ... } // operationId: "findAll"
@Get(':id')
findOne(): UserDto { ... } // operationId: "findOne"
@Post()
create(): UserDto { ... } // operationId: "create"
@Delete(':id')
remove(): void { ... } // operationId: "remove"
}Use descriptive method names — they serve double duty as both your code's API and the OpenAPI operation identifiers that SDK generators and documentation tools rely on.
Tags
Tags are automatically derived from the controller class name by stripping the Controller suffix:
| Class Name | Tag |
|---|---|
UsersController | Users |
AuthController | Auth |
OrderItemsController | OrderItems |
Override the tag with the @tag JSDoc tag:
/**
* @tag Authentication
*/
@Controller('auth')
export class AuthController {
// All operations tagged "Authentication" instead of "Auth"
}You can also apply @tag to individual methods:
@Controller('users')
export class UsersController {
/**
* @tag Admin
*/
@Delete(':id')
remove(@Param('id') id: string): void { ... }
// Tagged "Admin" instead of "Users"
}Custom Status Codes
Use @HttpCode() to override the default status code:
@Controller('users')
export class UsersController {
@Post()
@HttpCode(200) // Override POST's default 201
login(@Body() body: LoginDto): TokenDto { ... }
@Delete(':id')
@HttpCode(204) // No content
remove(@Param('id') id: string): void { ... }
}JSDoc Metadata
JSDoc comments on controller methods map directly to OpenAPI operation fields.
Summary and Description
/**
* Retrieves a paginated list of users with optional filtering.
*
* @summary List users
*/
@Get()
findAll(@Query() query: PaginationDto): UserDto[] { ... }@summary→summaryfield in OpenAPI- The JSDoc body text (first paragraph) →
descriptionfield - You can also use
@descriptionexplicitly
Deprecated
/**
* @deprecated Use findAllV2 instead.
*/
@Get()
findAll(): UserDto[] { ... }Marks the operation as deprecated: true in the OpenAPI document.
Hidden / Exclude
/**
* @hidden
*/
@Get('internal/health')
healthCheck(): string { ... }Routes marked with @hidden or @exclude are omitted entirely from the generated OpenAPI document. Use this for internal endpoints that shouldn't appear in public API docs.
Security
/**
* @security bearer
*/
@Get('profile')
getProfile(): UserDto { ... }For OAuth2 scopes:
/**
* @security oauth2 read:users write:users
*/
@Get()
findAll(): UserDto[] { ... }This adds the security field to the operation. The referenced security scheme must be defined in your OpenAPI config.
Error Responses
/**
* @throws {400} ValidationError
* @throws {404} NotFoundError
* @throws {403} ForbiddenError
*/
@Get(':id')
findOne(@Param('id') id: string): UserDto { ... }Each @throws tag generates an additional response entry in the operation. The syntax is @throws {statusCode} TypeName, where TypeName refers to a class or interface in your codebase.
Server-Sent Events
@EventStream (recommended)
The @EventStream() decorator from @tsgonest/runtime provides type-safe SSE with discriminated event unions, compile-time validation/serialization, and automatic OpenAPI schemas:
import { EventStream, SseEvent, SseEvents } from '@tsgonest/runtime';
type OrderEvents = SseEvents<{
created: OrderDto;
shipped: OrderDto;
cancelled: { id: string; reason: string };
}>;
@Controller('orders')
export class OrdersController {
@EventStream('events', { heartbeat: 30_000 })
async *events(): AsyncGenerator<OrderEvents> { ... }
// GET /orders/events
// Response: text/event-stream with discriminated oneOf itemSchema
}See the dedicated Server-Sent Events page for full documentation.
@Sse (legacy)
NestJS's built-in @Sse() decorator is also supported. It maps to a GET operation with text/event-stream content type:
@Controller('notifications')
export class NotificationsController {
@Sse('stream')
stream(): Observable<MessageEvent> { ... }
// GET /notifications/stream
// Response: text/event-stream with itemSchema
}See Return Types — SSE Endpoints for more on how @Sse() response schemas are generated.
Comprehensive Example
/**
* @tag Orders
*/
@Controller('orders')
export class OrdersController {
/**
* Returns a paginated list of orders for the authenticated user.
*
* @summary List orders
* @security bearer
*/
@Get()
findAll(@Query() query: OrderFilterDto): PaginatedResponse<OrderDto> {
// ...
}
/**
* Retrieve a single order by ID.
*
* @summary Get order
* @security bearer
* @throws {404} NotFoundError
*/
@Get(':id')
findOne(@Param('id') id: string): OrderDto {
// ...
}
/**
* Place a new order.
*
* @summary Create order
* @security bearer
* @throws {400} ValidationError
*/
@Post()
create(@Body() body: CreateOrderDto): OrderDto {
// ...
}
/**
* Cancel an order. Only pending orders can be cancelled.
*
* @summary Cancel order
* @security bearer
* @throws {404} NotFoundError
* @throws {409} ConflictError
*/
@Delete(':id')
@HttpCode(204)
cancel(@Param('id') id: string): void {
// ...
}
/**
* @hidden
*/
@Post('internal/recalculate')
recalculate(@Body() body: RecalculateDto): void {
// ...
}
}This produces six OpenAPI operations under the /orders path with proper tags, security requirements, error responses, and the internal endpoint excluded from the document.