api routes
Learn how Duck Gen scans NestJS controllers and generates fully typed route maps with request and response types.
Overview
The API routes extension scans your NestJS controllers and generates a TypeScript type map where each key is a route path and each value describes the HTTP method, request shape, and response type. This gives your client code compile-time knowledge of every route your server exposes.
Duck Gen is designed for multiple frameworks. This guide documents the NestJS adapter, the first adapter shipped and currently tested.
After running duck-gen, your client code knows every route path, what data to send, and
what data comes back, all enforced by TypeScript. Misspell a route? Type error. Send the
wrong body shape? Type error. Forget a required param? Type error.
What it scans
Duck Gen looks for three things in your source files:
- Controller classes: any class decorated with
@Controller(). - HTTP method decorators: methods inside controllers decorated with
@Get,@Post, etc. - Parameter decorators:
@Body,@Query,@Param, and@Headerson method parameters.
@Controller('users') // controller path
export class UsersController {
@Get(':id') // HTTP method + method path
findOne(
@Param('id') id: string, // parameter decorator
@Query() query: FindUserQuery,
): Promise<UserDto> { // return type becomes response type
return this.usersService.findOne(id, query)
}
}From this, Duck Gen generates a route entry for /api/users/:id (assuming globalPrefix: "/api")
with the GET method, params: { id: string }, query: FindUserQuery, and response type UserDto.
Supported decorators
Controller decorator
| Decorator | Description |
|---|---|
@Controller() | No path prefix, routes start from globalPrefix. |
@Controller('users') | Adds /users prefix to all routes in this controller. |
@Controller('admin/users') | Nested paths work too. |
HTTP method decorators
| Decorator | HTTP Method |
|---|---|
@Get() | GET |
@Post() | POST |
@Put() | PUT |
@Patch() | PATCH |
@Delete() | DELETE |
@Options() | OPTIONS |
@Head() | HEAD |
@All() | ALL |
Each decorator accepts an optional string path:
@Get() // matches the controller path exactly
@Get('list') // appends /list to the controller path
@Get(':id') // appends /:id (path parameter)
@Post(':id/ban') // appends /:id/banParameter decorators
| Decorator | Maps to | Required? | Example type |
|---|---|---|---|
@Body() | body | yes (POST/PUT/PATCH) | The full DTO type |
@Body('field') | body.field | no (optional field) | { field?: Type } |
@Query() | query | no | The full query DTO |
@Query('page') | query.page | no (optional field) | { page?: Type } |
@Param('id') | params.id | yes | { id: Type } |
@Headers() | headers | no | The full headers type |
@Headers('x-token') | headers['x-token'] | no (optional field) | { 'x-token'?: Type } |
When you use @Body() (no argument), the entire parameter type becomes the body type.
When you use @Body('email'), Duck Gen creates { email?: Type }, an object with that
single optional field. The same logic applies to @Query and @Headers.
Path construction rules
Duck Gen builds the final route path by joining three segments:
Loading diagram...
For example:
| globalPrefix | Controller | Method | Result |
|---|---|---|---|
/api | @Controller('users') | @Get(':id') | /api/users/:id |
/api | @Controller('admin/users') | @Post() | /api/admin/users |
| (none) | @Controller('auth') | @Post('signin') | /auth/signin |
/api/v2 | @Controller() | @Get('health') | /api/v2/health |
Rules:
- Empty segments are removed.
- Slashes are normalized (no double slashes).
- Only string literal paths are supported. If a decorator uses a variable or expression, that route is skipped.
Request shape rules
Each route's request type is an object with up to four fields: body, query, params,
and headers. Fields that don't apply to a route are set to never and cleaned up
by Duck Query's CleanupNever utility.
@Body: request body
Full body: when @Body() is used without a property key, the parameter type becomes
the entire body type:
@Post('signin')
signin(@Body() body: SigninDto): Promise<AuthSession> { ... }
// => body: SigninDtoNamed property: when @Body('field') is used, Duck Gen creates an object with that
field as optional:
@Post('update')
update(@Body('email') email: string, @Body('name') name: string): Promise<UserDto> { ... }
// => body: { email?: string } & { name?: string }@Query: query string
Works the same as @Body:
@Get('search')
search(@Query() query: SearchDto): Promise<SearchResult[]> { ... }
// => query: SearchDto
@Get('list')
list(@Query('page') page: number, @Query('limit') limit: number): Promise<UserDto[]> { ... }
// => query: { page?: number } & { limit?: number }@Param: path parameters
Path params are always required (not optional):
@Get(':id')
findOne(@Param('id') id: string): Promise<UserDto> { ... }
// => params: { id: string }
@Get(':userId/posts/:postId')
findPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
): Promise<PostDto> { ... }
// => params: { userId: string } & { postId: string }@Headers: request headers
@Get('me')
me(@Headers('authorization') auth: string): Promise<UserDto> { ... }
// => headers: { authorization?: string }Multiple decorators of the same kind
When a method has multiple decorators of the same kind, Duck Gen merges them using intersection types:
@Post(':id/transfer')
transfer(
@Param('id') id: string,
@Body('amount') amount: number,
@Body('to') to: string,
@Query('confirm') confirm: boolean,
): Promise<TransferResult> { ... }
// => params: { id: string }
// => body: { amount?: number } & { to?: string }
// => query: { confirm?: boolean }Response types
Duck Gen uses the method's return type as the response type. If your method returns
Promise<T>, the response type is T:
@Get(':id')
findOne(@Param('id') id: string): Promise<UserDto> {
return this.service.findOne(id)
}
// => response type: UserDtoAlways add explicit return type annotations to your controller methods. If the return type
is inferred as any, Duck Gen will print a warning and convert it to unknown (when
normalizeAnyToUnknown is enabled). Explicit types give you the best results.
Complete example
Here is a full controller with multiple routes, and what Duck Gen generates from it:
The controller
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common'
@Controller('users')
export class UsersController {
constructor(private readonly service: UsersService) {}
@Get()
findAll(@Query() query: PaginationDto): Promise<UserDto[]> {
return this.service.findAll(query)
}
@Get(':id')
findOne(@Param('id') id: string): Promise<UserDto> {
return this.service.findOne(id)
}
@Post()
create(@Body() body: CreateUserDto): Promise<UserDto> {
return this.service.create(body)
}
@Put(':id')
update(
@Param('id') id: string,
@Body() body: UpdateUserDto,
): Promise<UserDto> {
return this.service.update(id, body)
}
@Delete(':id')
remove(@Param('id') id: string): Promise<void> {
return this.service.remove(id)
}
}The generated types
With globalPrefix: "/api", Duck Gen produces:
import type { PaginationDto } from '../../src/modules/users/users.dto'
import type { CreateUserDto } from '../../src/modules/users/users.dto'
import type { UpdateUserDto } from '../../src/modules/users/users.dto'
import type { UserDto } from '../../src/modules/users/users.dto'
export interface ApiRoutes {
'/api/users': {
body: never
query: PaginationDto
params: never
headers: never
res: UserDto[]
method: 'GET'
}
'/api/users/:id': {
body: never
query: never
params: { id: string }
headers: never
res: UserDto
method: 'GET'
} | {
body: UpdateUserDto
query: never
params: { id: string }
headers: never
res: UserDto
method: 'PUT'
} | {
body: never
query: never
params: { id: string }
headers: never
res: void
method: 'DELETE'
}
'/api/users': {
body: CreateUserDto
query: never
params: never
headers: never
res: UserDto
method: 'POST'
}
}Using the types on the client
import type { RouteReq, RouteRes } from '@gentleduck/gen/nestjs'
// Extract types for a specific route
type CreateUserReq = RouteReq<'/api/users'>
// => { body: CreateUserDto }
type UserResponse = RouteRes<'/api/users/:id'>
// => UserDto
// Use with Duck Query for fully typed HTTP calls
import { createDuckQueryClient } from '@gentleduck/query'
import type { ApiRoutes } from '@gentleduck/gen/nestjs'
const client = createDuckQueryClient<ApiRoutes>({
baseURL: 'http://localhost:3000',
})
// TypeScript enforces the correct request shape
const { data: users } = await client.get('/api/users', {
query: { page: 1, limit: 10 },
})
const { data: user } = await client.get('/api/users/:id', {
params: { id: 'u_123' },
})
const { data: created } = await client.post('/api/users', {
body: { email: 'duck@example.com', name: 'Duck' },
})Edge cases and notes
Dynamic paths are skipped
Duck Gen only supports string literal paths. This will be skipped:
const ROUTE = 'users'
@Controller(ROUTE) // skipped, not a string literal
export class UsersController { ... }Duck Gen runs at build time and doesn't execute your code. It reads the AST (abstract syntax tree) to find decorator arguments. Variables and expressions would require runtime evaluation, which isn't possible during static analysis. Use string literals in your decorators for reliable type generation.
Methods without HTTP decorators are ignored
Only methods with recognized HTTP decorators (@Get, @Post, etc.) are included.
Private methods, lifecycle hooks, and helper methods are not scanned.
Complex return types
Duck Gen handles complex return types including:
- Generic types like
Promise<T> - Union types like
UserDto | null - Drizzle ORM's
InferReturningand similar utility types - Intersection types
- Array types
The type expander recursively resolves these into their concrete shapes.
Multiple controllers for the same module
Duck Gen scans all controllers in your project. If you have an admin controller and a public controller for the same resource, both sets of routes appear in the generated map:
@Controller('users')
export class UsersController { ... }
@Controller('admin/users')
export class AdminUsersController { ... }
// Both generate routes: /api/users/... and /api/admin/users/...Next steps
- Configuration reference: customize scanning and output behavior.
- Generated types reference: learn every exported type.
- Messages guide: generate typed i18n dictionaries.
- Duck Query: use generated types with a type-safe HTTP client.