Skip to main content

type-safe apis with duck gen

How Duck Gen eliminates the gap between your server routes and client code, covering how the compiler extension works.

The Type Gap

Server and client end up speaking different type languages.

You define a DTO on the server, write a matching type on the client, and someone renames a field. The client keeps compiling. The bug ships.

Duck Gen closes the gap.


The Workflow

Loading diagram...

Step 1: Write your server code normally

// src/auth/auth.controller.ts
@Controller('auth')
export class AuthController {
  @Post('signin')
  signin(@Body() body: SigninDto): Promise<AuthSession> {
    return this.authService.signin(body)
  }

  @Post('signup')
  signup(@Body() body: SignupDto): Promise<AuthSession> {
    return this.authService.signup(body)
  }
}

Step 2: Generate types

bunx @gentleduck/gen

Duck Gen parses your sources with ts-morph, finds every @Controller, and extracts:

  • HTTP method decorators (@Get, @Post, @Put, @Delete, @Patch)
  • Route paths (controller prefix + method path)
  • Parameter decorators (@Body, @Query, @Param, @Headers)
  • Return types

Step 3: Use typed routes on the client

import { createDuckQuery } from '@gentleduck/query'
import type { ApiRoutes } from './generated/api-routes'

const client = createDuckQuery<ApiRoutes>({
  baseURL: 'https://api.example.com',
})

// Fully typed: body shape, response type, route path
const session = await client.post('/api/auth/signin', {
  body: { email: 'user@example.com', password: 'secret' },
})
// session.data is AuthSession

What Gets Generated

The output is a single .d.ts file with a complete route map:

// generated/api-routes.d.ts: do not edit
export interface ApiRoutes {
  '/api/auth/signin': {
    POST: { body: SigninDto; res: AuthSession }
  }
  '/api/auth/signup': {
    POST: { body: SignupDto; res: AuthSession }
  }
  '/api/users/:id': {
    GET: { params: { id: string }; res: UserProfile }
    PATCH: { params: { id: string }; body: UpdateUserDto; res: UserProfile }
  }
}

A pure type file: no runtime code, no bundle impact. It disappears at compile time.


Configuration

{
  "framework": "nestjs",
  "srcDir": "./src",
  "outputDir": "./generated",
  "apiPrefix": "/api",
  "include": ["**/*.controller.ts"],
  "exclude": ["**/*.spec.ts"]
}

Save this as duck-gen.json in your project root. Duck Gen reads it automatically.


Why Not tRPC / GraphQL Codegen?

Duck GentRPCGraphQL Codegen
Server frameworkNestJS (REST)Custom routerGraphQL servers
Client couplingNone, types onlyTight couplingSchema-first
Runtime costZero (.d.ts only)MinimalVaries
Migration effortDrop-in, no server changesFull rewriteSchema migration
Existing REST APIsWorks immediatelyNot applicableNot applicable

Duck Gen is for teams with existing REST APIs that want type safety without a server rewrite.


Getting Started

bun add -d @gentleduck/gen    # Dev dependency, runs at build time
bun add @gentleduck/query     # Runtime, typed HTTP client