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 --noEmitThis 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.
Both approaches work. If you commit generated files, client developers see type changes
immediately on pull. If you do not commit them, each developer and CI must run 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.tsSet 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:watchThis 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, and more. |
| Templates | Complete working example with auth flow. |