Chapter 3: Your First Controller
Build a NestJS controller with typed DTOs and decorators that Duck Gen can scan.
What Duck Gen scans
Duck Gen looks for two things in your source code:
- Controller classes decorated with
@Controller(). - HTTP method decorators like
@Get(),@Post(),@Put(),@Patch(),@Delete().
For each decorated method, it extracts the route path, request shape, and response type. Let us build a controller that demonstrates all of these.
Create a Users module
First, create the file structure for a Users feature:
mkdir -p src/users/dtoDefine your DTOs
DTOs (Data Transfer Objects) describe the shape of data your API accepts and returns. Duck Gen reads these types directly from your code.
export class CreateUserDto {
name: string
email: string
password: string
}export class UpdateUserDto {
name?: string
email?: string
}export class UserDto {
id: string
name: string
email: string
createdAt: string
}export class PaginationDto {
page?: number
limit?: number
}NestJS uses classes for DTOs because they exist at runtime, which is needed for validation pipes. Duck Gen reads the type information from these classes at compile time, so it works with both classes and interfaces.
Define a response wrapper
Most APIs wrap responses in a consistent shape. Create a generic response type:
export class ApiResponse<TData, TMessage extends string = string> {
data: TData
message: TMessage
success: boolean
}Duck Gen fully resolves generics, so ApiResponse<UserDto, UserMessage> in a return type
will generate the expanded type in the output.
Build the controller
Now create the Users controller with full CRUD operations:
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'
import { ApiResponse } from '../common/api-response'
import { CreateUserDto } from './dto/create-user.dto'
import { PaginationDto } from './dto/pagination.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { UserDto } from './dto/user.dto'
type UserMessage = 'USER_CREATED' | 'USER_UPDATED' | 'USER_DELETED' | 'USER_FOUND' | 'USERS_LISTED'
@Controller('users')
export class UsersController {
@Get()
findAll(@Query() query: PaginationDto): Promise<ApiResponse<UserDto[], UserMessage>> {
// Duck Gen extracts:
// - Method: GET
// - Path: /api/users
// - Query: PaginationDto
// - Response: ApiResponse<UserDto[], UserMessage>
return {} as any
}
@Get(':id')
findOne(@Param('id') id: string): Promise<ApiResponse<UserDto, UserMessage>> {
// Duck Gen extracts:
// - Method: GET
// - Path: /api/users/:id
// - Params: { id: string }
// - Response: ApiResponse<UserDto, UserMessage>
return {} as any
}
@Post()
create(@Body() body: CreateUserDto): Promise<ApiResponse<UserDto, UserMessage>> {
// Duck Gen extracts:
// - Method: POST
// - Path: /api/users
// - Body: CreateUserDto
// - Response: ApiResponse<UserDto, UserMessage>
return {} as any
}
@Put(':id')
update(
@Param('id') id: string,
@Body() body: UpdateUserDto,
): Promise<ApiResponse<UserDto, UserMessage>> {
// Duck Gen extracts:
// - Method: PUT
// - Path: /api/users/:id
// - Params: { id: string }
// - Body: UpdateUserDto
// - Response: ApiResponse<UserDto, UserMessage>
return {} as any
}
@Delete(':id')
remove(@Param('id') id: string): Promise<ApiResponse<null, UserMessage>> {
// Duck Gen extracts:
// - Method: DELETE
// - Path: /api/users/:id
// - Params: { id: string }
// - Response: ApiResponse<null, UserMessage>
return {} as any
}
}Register the controller
Add the controller to a module and import it in your app:
import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
@Module({
controllers: [UsersController],
})
export class UsersModule {}import { Module } from '@nestjs/common'
import { UsersModule } from './users/users.module'
@Module({
imports: [UsersModule],
})
export class AppModule {}What Duck Gen sees
When you run duck-gen, it finds UsersController and extracts five routes:
| Method | Path | Body | Query | Params | Response |
|---|---|---|---|---|---|
| GET | /api/users | - | PaginationDto | - | ApiResponse<UserDto[], UserMessage> |
| GET | /api/users/:id | - | - | { id: string } | ApiResponse<UserDto, UserMessage> |
| POST | /api/users | CreateUserDto | - | - | ApiResponse<UserDto, UserMessage> |
| PUT | /api/users/:id | UpdateUserDto | - | { id: string } | ApiResponse<UserDto, UserMessage> |
| DELETE | /api/users/:id | - | - | { id: string } | ApiResponse<null, UserMessage> |
Each route becomes an entry in the generated route map with fully resolved types.
Key rules for Duck Gen scanning
-
Use string literals in decorators.
@Controller('users')works.@Controller(ROUTE_VAR)does not, because Duck Gen cannot resolve runtime values. -
Add explicit return types. If a method has no return type annotation, Duck Gen infers
anyand warns you. Always annotate your return types. -
Use standard NestJS parameter decorators.
@Body(),@Query(),@Param(), and@Headers()are all supported.