configuration
Complete reference for every duck-gen.json configuration option with examples and explanations.
Overview
Duck Gen reads config from a duck-gen.json file at the project root. The file is
required — the CLI fails if it can't find or parse it.
All paths resolve relative to the config file.
Add a $schema field to get autocomplete, validation, and inline docs in VS Code or any
editor that reads JSON Schema.
Minimal config
The smallest working configuration for a NestJS project:
{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"outputSource": "./generated",
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true,
"outputSource": "./generated"
},
"messages": {
"enabled": true,
"outputSource": "./generated"
}
}
}{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"outputSource": "./generated",
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true,
"outputSource": "./generated"
},
"messages": {
"enabled": true,
"outputSource": "./generated"
}
}
}JSON Schema
Add $schema for autocomplete and validation:
{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json"
}{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json"
}The schema ships with the @gentleduck/gen package.
Root options
framework
{ "framework": "nestjs" }{ "framework": "nestjs" }| Property | Value |
|---|---|
| Type | string |
| Required | Yes |
| Accepted values | "nestjs" |
The target framework adapter. Only nestjs is supported today — other values fail
validation. More adapters are planned.
Shared options
extensions.shared applies to both the API routes and messages extensions.
Values under extensions.shared affect every extension. An outputSource here writes
both route types and message types to that location. Use the per-extension
outputSource to split them.
tsconfigPath
{
"extensions": {
"shared": {
"tsconfigPath": "./tsconfig.json"
}
}
}{
"extensions": {
"shared": {
"tsconfigPath": "./tsconfig.json"
}
}
}| Property | Value |
|---|---|
| Type | string |
| Required | Yes (for NestJS) |
| Default | None |
Path to tsconfig.json. Duck Gen uses ts-morph to build an
in-memory TypeScript project from this config. The include and exclude globs pick
which files get scanned.
The tsconfig must include the files you want scanned. Anything excluded by tsconfig stays invisible to Duck Gen.
includeNodeModules
{
"extensions": {
"shared": {
"includeNodeModules": false
}
}
}{
"extensions": {
"shared": {
"includeNodeModules": false
}
}
}| Property | Value |
|---|---|
| Type | boolean |
| Required | Yes |
| Recommended | false |
Include node_modules during scanning. Keep this false unless you need to scan
third-party code (for example, a linked workspace package).
includeNodeModules: true makes Duck Gen parse every TypeScript file under
node_modules. Only enable it to scan controllers in a linked workspace package.
outputSource
{
"extensions": {
"shared": {
"outputSource": "./generated"
}
}
}{
"extensions": {
"shared": {
"outputSource": "./generated"
}
}
}| Property | Value |
|---|---|
| Type | string or string[] |
| Required | No |
| Default | None |
Extra output directories for all generated files (both API routes and messages).
Duck Gen always writes to its internal generated folder inside the package. This
option adds more locations.
Single directory:
{ "outputSource": "./generated" }{ "outputSource": "./generated" }Multiple directories:
{ "outputSource": ["./generated", "../shared-types/generated"] }{ "outputSource": ["./generated", "../shared-types/generated"] }A directory path (no extension) creates files with default names inside it. A file path
(with .d.ts) writes directly to that file.
sourceGlobs
{
"extensions": {
"shared": {
"sourceGlobs": ["src/**/*.ts", "src/**/*.tsx"]
}
}
}{
"extensions": {
"shared": {
"sourceGlobs": ["src/**/*.ts", "src/**/*.tsx"]
}
}
}| Property | Value |
|---|---|
| Type | string[] |
| Required | No |
| Status | Reserved, present in the schema but not applied by the current NestJS adapter |
The NestJS adapter uses the tsconfig include/exclude globs instead. This field is reserved for future adapters that don't rely on a tsconfig.
API Routes options
extensions.apiRoutes configures the API route type generator.
enabled
{
"extensions": {
"apiRoutes": {
"enabled": true
}
}
}{
"extensions": {
"apiRoutes": {
"enabled": true
}
}
}| Property | Value |
|---|---|
| Type | boolean |
| Required | Yes |
Turns API route generation on or off. Set false to generate only message types.
globalPrefix
{
"extensions": {
"apiRoutes": {
"globalPrefix": "/api"
}
}
}{
"extensions": {
"apiRoutes": {
"globalPrefix": "/api"
}
}
}| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None (empty prefix) |
Prefix applied to all generated routes. Match the global prefix set in the NestJS app:
const app = await NestFactory.create(AppModule)
app.setGlobalPrefix('api') // match this in duck-gen.json as "/api"const app = await NestFactory.create(AppModule)
app.setGlobalPrefix('api') // match this in duck-gen.json as "/api"Without a prefix, routes start directly from the controller path:
- With
globalPrefix: "/api":/api/users/:id - Without:
/users/:id
If the NestJS app uses app.setGlobalPrefix('api') and the config says
globalPrefix: "/v1", generated paths won't match real server paths. Keep them aligned.
normalizeAnyToUnknown
{
"extensions": {
"apiRoutes": {
"normalizeAnyToUnknown": true
}
}
}{
"extensions": {
"apiRoutes": {
"normalizeAnyToUnknown": true
}
}
}| Property | Value |
|---|---|
| Type | boolean |
| Required | Yes |
| Recommended | true |
When enabled:
anyreturn types becomeunknownin generated output.- Each method with an
anyreturn type logs a warning.
This keeps any from leaking into client types. Set false only to let any through
on purpose.
outputSource (apiRoutes)
{
"extensions": {
"apiRoutes": {
"outputSource": "./generated"
}
}
}{
"extensions": {
"apiRoutes": {
"outputSource": "./generated"
}
}
}| Property | Value |
|---|---|
| Type | string or string[] |
| Required | No |
Extra output locations for API route types only. Same semantics as
shared.outputSource, scoped to the routes file.
Adds to shared.outputSource — both sets of locations receive the file.
Messages options
extensions.messages configures the message registry type generator.
enabled
{
"extensions": {
"messages": {
"enabled": true
}
}
}{
"extensions": {
"messages": {
"enabled": true
}
}
}| Property | Value |
|---|---|
| Type | boolean |
| Required | Yes |
Turns message generation on or off. Set false to generate only API route types.
outputSource (messages)
{
"extensions": {
"messages": {
"outputSource": "./generated"
}
}
}{
"extensions": {
"messages": {
"outputSource": "./generated"
}
}
}| Property | Value |
|---|---|
| Type | string or string[] |
| Required | No |
Extra output locations for message types only. Same semantics as the API routes
outputSource.
Complete example
A fully configured duck-gen.json with every option set:
{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"outputSource": ["./generated", "../client/generated"],
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true,
"outputSource": "./generated"
},
"messages": {
"enabled": true,
"outputSource": "./generated"
}
}
}{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"outputSource": ["./generated", "../client/generated"],
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true,
"outputSource": "./generated"
},
"messages": {
"enabled": true,
"outputSource": "./generated"
}
}
}With this config:
-
API route types are written to:
node_modules/@gentleduck/gen/generated/nestjs/duck-gen-api-routes.d.ts(always)./generated/duck-gen-api-routes.d.ts(fromshared.outputSource)../client/generated/duck-gen-api-routes.d.ts(fromshared.outputSource)./generated/duck-gen-api-routes.d.ts(fromapiRoutes.outputSource, deduped)
-
Message types are written to the same locations but as
duck-gen-messages.d.ts.
Configuration examples
API routes only
For projects without i18n or message keys:
{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true
},
"messages": {
"enabled": false
}
}
}{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true
},
"messages": {
"enabled": false
}
}
}Messages only
For projects that only need i18n type safety:
{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": false
},
"messages": {
"enabled": true,
"outputSource": "./generated"
}
}
}{
"$schema": "node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": false
},
"messages": {
"enabled": true,
"outputSource": "./generated"
}
}
}Monorepo with shared output
When server and client live in separate packages:
{
"$schema": "../../node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"outputSource": ["./generated", "../../packages/shared-types/generated"],
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true
},
"messages": {
"enabled": true
}
}
}{
"$schema": "../../node_modules/@gentleduck/gen/duck-gen.schema.json",
"framework": "nestjs",
"extensions": {
"shared": {
"includeNodeModules": false,
"outputSource": ["./generated", "../../packages/shared-types/generated"],
"sourceGlobs": ["src/**/*.ts"],
"tsconfigPath": "./tsconfig.json"
},
"apiRoutes": {
"enabled": true,
"globalPrefix": "/api",
"normalizeAnyToUnknown": true
},
"messages": {
"enabled": true
}
}
}Then, in the client package:
import type { ApiRoutes } from '../../packages/shared-types/generated/duck-gen-api-routes'import type { ApiRoutes } from '../../packages/shared-types/generated/duck-gen-api-routes'With a shared output directory, add the generated files to .gitignore and regenerate
them in CI. Fewer merge conflicts, always-fresh types.
Next steps
- API Routes guide: how route scanning works.
- Messages guide: how message scanning works.
- Generated types: every exported type explained.