Skip to main content
Search...

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:

  1. Controller classes decorated with @Controller().
  2. 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.

src/users/dto/create-user.dto.ts
export class CreateUserDto {
  name: string
  email: string
  password: string
}
src/users/dto/update-user.dto.ts
export class UpdateUserDto {
  name?: string
  email?: string
}
src/users/dto/user.dto.ts
export class UserDto {
  id: string
  name: string
  email: string
  createdAt: string
}
src/users/dto/pagination.dto.ts
export class PaginationDto {
  page?: number
  limit?: number
}

Define a response wrapper

Most APIs wrap responses in a consistent shape. Create a generic response type:

src/common/api-response.ts
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:

src/users/users.controller.ts
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:

src/users/users.module.ts
import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
 
@Module({
  controllers: [UsersController],
})
export class UsersModule {}
src/app.module.ts
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:

MethodPathBodyQueryParamsResponse
GET/api/users-PaginationDto-ApiResponse<UserDto[], UserMessage>
GET/api/users/:id--{ id: string }ApiResponse<UserDto, UserMessage>
POST/api/usersCreateUserDto--ApiResponse<UserDto, UserMessage>
PUT/api/users/:idUpdateUserDto-{ 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

  1. Use string literals in decorators. @Controller('users') works. @Controller(ROUTE_VAR) does not, because Duck Gen cannot resolve runtime values.

  2. Add explicit return types. If a method has no return type annotation, Duck Gen infers any and warns you. Always annotate your return types.

  3. Use standard NestJS parameter decorators. @Body(), @Query(), @Param(), and @Headers() are all supported.

Next

Chapter 4: Generating Types