Chapter 8: Real World Patterns
Production patterns for error handling, interceptors, CI/CD integration, and team workflows.
Error handling
Duck Query returns Axios responses, so you can catch errors the same way you would with plain Axios:
import { AxiosError } from 'axios'
import { api } from './client'
async function createUser(body: { name: string; email: string; password: string }) {
try {
const result = await api.post('/api/users', { body })
return { success: true, data: result }
} catch (error) {
if (error instanceof AxiosError) {
// error.response?.data contains the server error response
// error.response?.status contains the HTTP status code
return { success: false, error: error.response?.data }
}
throw error
}
}
Typed error responses
If your server uses consistent error shapes, you can type them:
interface ApiError {
message: string
statusCode: number
errors?: Record<string, string[]>
}
function isApiError(data: unknown): data is ApiError {
return typeof data === 'object' && data !== null && 'statusCode' in data
}
Request interceptors
Duck Query exposes the underlying Axios instance, so you can add interceptors for authentication, logging, or retry logic:
import { createDuckQuery } from '@gentleduck/duck-query'
import type { ApiRoutes } from '@gentleduck/gen/nestjs'
export const api = createDuckQuery<ApiRoutes>({
baseURL: 'http://localhost:3000',
})
// Add auth token to every request
api.axios.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Handle 401 responses globally
api.axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token')
window.location.href = '/login'
}
return Promise.reject(error)
},
)
CI/CD integration
Duck Gen should run as part of your build pipeline to ensure types are always up to date.
GitHub Actions example
name: Type Check
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Generate types
run: bun run generate
- name: Type check
run: bun run tsc --noEmit
This pipeline:
- Installs dependencies (including
@gentleduck/gen). - Runs
duck-gento generate fresh types from the current server code. - Runs TypeScript's type checker. If any client code uses outdated types, the pipeline fails.
Pre-commit hook
You can also run Duck Gen before every commit to keep types in sync:
{
"scripts": {
"precommit": "bun run generate && git add generated/"
}
}
Team workflow
Here is a recommended workflow for teams using Duck Gen:
- Server developer changes a DTO or adds a new route.
- Server developer runs
bun run generatelocally. - Server developer commits both the source changes and the regenerated types.
- Client developer pulls the latest changes. TypeScript immediately shows any breaking type changes in their client code.
- CI pipeline runs
duck-gen+tscto verify everything is in sync.
duck-gen
after installing dependencies. Most teams prefer committing the generated files for
faster feedback.Monorepo setup
In a monorepo where the server and client are separate packages, Duck Gen works across package boundaries:
packages/
server/
src/
duck-gen.json
package.json
client/
src/
package.json
generated/ # shared output directory
duck-gen-api-routes.d.ts
duck-gen-messages.d.ts
Set outputSource in duck-gen.json to point to a shared directory:
{
"extensions": {
"shared": {
"outputSource": "../generated"
}
}
}
The client package can then import from the shared directory or from @gentleduck/gen/nestjs.
Watch mode
During active development, use watch mode to regenerate types whenever your server code changes:
bun run generate:watch
This is especially useful when a server developer and a client developer are working on the same feature simultaneously.
What you have learned
Over these eight chapters, you have:
- Understood why the type safety gap between server and client exists.
- Set up a NestJS project with Duck Gen configured.
- Built controllers with typed DTOs and decorators.
- Generated route types and explored the output files.
- Imported and used generated types for type-safe API calls.
- Added i18n message registries with
@duckgentags. - Set up Duck Query for a fully typed HTTP client.
- Learned production patterns for error handling, CI/CD, and team workflows.
Where to go next
| Resource | What it covers |
|---|---|
| Configuration reference | Every duck-gen.json option explained in detail. |
| API Routes reference | Advanced scanning rules, edge cases, and all supported decorators. |
| Messages reference | Advanced message patterns, multiple groups, and troubleshooting. |
| Generated Types reference | Every exported type with full documentation. |
| Duck Query reference | Complete Duck Query API reference. |
| Advanced patterns | Custom Axios instances, SSR, retry logic. |
| Templates | Complete working example with auth flow. |