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/dto
Define 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
}
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.