Logonestjs-openapi
Guides

Configuration

Every option in the nestjs-openapi config file: output path and format, entry module, tsconfig, OpenAPI info, servers, security schemes and filters.

All configuration is done through a TypeScript config file (typically openapi.config.ts). Use the defineConfig helper for type safety.

Config File Structure

import {  } from 'nestjs-openapi';

export default ({
  // Where to write the spec (required)
  : 'openapi.json',

  // Output format
  : 'json',

  // Input file configuration
  : {
    : 'src/app.module.ts',
  },

  // OpenAPI metadata
  : {
    : { : 'My API', : '1.0.0' },
  },

  // Generation options
  : {
    : '/api',
  },
});

output (required)

Path to write the generated specification. Relative to the config file.

output: 'src/openapi/spec.json';

format

Output format for the specification.

ValueDescription
'json'JSON format (default)
'yaml'YAML format (fully supported)

files

Input file configuration. All paths are relative to the config file location.

files: {
   // Entry module file(s) - default: 'src/app.module.ts'
   entry: 'src/app.module.ts',

   // Multiple entry points (only the first is used today)
   entry: ['src/app.module.ts', 'src/admin.module.ts'],

   // tsconfig path (recommended)
   tsconfig: 'tsconfig.json',

  // Glob pattern(s) for DTO files
  dtoGlob: 'src/**/*.dto.ts',

  // Multiple DTO patterns
  dtoGlob: ['src/**/*.dto.ts', 'libs/**/*.dto.ts'],

  // Glob patterns to include
  include: ['src/**/*.ts'],

  // Glob patterns to exclude (defaults shown)
  exclude: ['**/*.spec.ts', '**/*.test.ts', '**/node_modules/**'],
}

Set tsconfig explicitly. The Promise-based generator will search upward from files.entry if omitted, but the Effect-based API requires a path.

Entry Points

The entry option specifies which module file to analyze. The library traverses @Module({ imports }) to discover all controllers.

// Single entry point
entry: 'src/app.module.ts';

// Multiple entry points (first entry is used today)
entry: ['apps/api/src/app.module.ts', 'apps/admin/src/app.module.ts'];

Multi-entry configs are normalized but the current generator only reads the first entry. Provide separate configs per app if you need multiple specs.

DTO Glob

The dtoGlob option specifies which files contain DTOs for schema generation. Only exported types are converted into schemas.

// Single pattern
dtoGlob: 'src/**/*.dto.ts';

// Multiple patterns
dtoGlob: ['src/**/*.dto.ts', 'src/**/*.entity.ts'];

Common patterns include **/*.dto.ts, **/*.entity.ts, **/*.model.ts, and **/*.schema.ts.

When a schema ref appears in paths but isn't generated from your globs, the generator tries to resolve the missing type by scanning for exported types and merges any additional schemas it finds. Keep dtoGlob focused for performance, and expand it if you still see broken ref warnings.

The CLI prints a broken refs warning if any $refs are missing. Use that list to adjust dtoGlob coverage.

openapi

OpenAPI specification metadata. Maps directly to the OpenAPI 3.0 spec structure.

info (required)

API metadata for the info section:

openapi: {
  info: {
    title: 'My API',           // required
    version: '1.0.0',          // required
    description: 'API for managing resources',
    contact: {
      name: 'API Support',
      email: 'support@example.com',
      url: 'https://support.example.com',
    },
    license: {
      name: 'MIT',
      url: 'https://opensource.org/licenses/MIT',
    },
  },
}

servers

Server URLs for the API:

openapi: {
  servers: [
    { url: 'https://api.example.com', description: 'Production' },
    { url: 'https://staging-api.example.com', description: 'Staging' },
    { url: 'http://localhost:3000', description: 'Development' },
  ],
}

tags

Tags for organizing API endpoints:

openapi: {
  tags: [
    { name: 'users', description: 'User management' },
    { name: 'posts', description: 'Blog post operations' },
    { name: 'auth', description: 'Authentication' },
  ],
}

security

Security schemes and global requirements. See the Security Guide for details.

openapi: {
  security: {
    schemes: [
      {
        name: 'bearerAuth',
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT',
      },
    ],
    global: [{ bearerAuth: [] }],
  },
}

options

Generation behavior options.

basePath

Global prefix for all routes. Equivalent to NestJS's app.setGlobalPrefix():

options: {
  basePath: '/api/v1',
}

With this configuration, a route GET /users becomes GET /api/v1/users.

transformPath

Rewrites each operation's path after basePath, with the operation's controller, method and HTTP method as context. Paths use OpenAPI syntax (/users/{id}). Combine it with openapi.servers to move part of the path into the server URL:

openapi: {
  // ...
  servers: [{ url: 'https://app.example.com/api' }],
},
options: {
  // @Controller('api/v1/audit-logs') is published as /v1/audit-logs
  transformPath: (path, { controller, method, httpMethod }) =>
    path.replace(/^\/api(?=\/|$)/, '') || '/',
}

operationExtensions

Adds x- extensions to each operation. The function gets what the generator knows about the operation and returns the extensions to add, or undefined for none:

FieldExample
httpMethod'post'
path'/api/users/{id}', after basePath and transformPath
operationId'UsersController_update'
controller, method'UsersController', 'update'
module'UsersModule', the module declaring the controller
entryThe files.entry the operation was found from, as written in the config
decorators.method, decorators.controllerThe decorators as written, top to bottom, each a { name, args }

Decorator arguments are evaluated statically, as for decorators: literals, arrays and objects become values, constants and enum members are resolved, and classes become a TypeRef. This is how your own decorators reach the spec:

options: {
  // @Roles('admin') becomes x-roles: ['admin']
  operationExtensions: ({ decorators }) => {
    const roles = [...decorators.controller, ...decorators.method]
      .filter((decorator) => decorator.name === 'Roles')
      .flatMap((decorator) => decorator.args);
    return roles.length > 0 ? { 'x-roles': roles } : undefined;
  },
}

Keys set with @ApiExtension on the method or its controller win over the returned ones. A returned name that does not start with x- fails generation.

versioning

The versioning you pass to app.enableVersioning(), which static analysis cannot see. With type: 'uri', paths get the version of their method (@Version()), else of their controller (@Controller({ version })), else defaultVersion, exactly as Nest builds them: /v1/users, one path per version for arrays, none for VERSION_NEUTRAL. Operation ids get the version too (UsersController_findAll_v1), as in @nestjs/swagger. Header, media-type and custom versioning do not change paths.

import { VERSION_NEUTRAL } from '@nestjs/common';

options: {
  versioning: {
    type: 'uri',
    prefix: 'v', // default; false for none
    defaultVersion: ['1', VERSION_NEUTRAL],
  },
}

enums

How enums are emitted (default: "ref").

ValueBehavior
"ref"Every TypeScript enum (and enum: Status without enumName) is a component named after it.
"nest"Like @nestjs/swagger: enum values are written in place, unless enumName gives them a component name.
// 'nest': { type: 'string', enum: ['low', 'high'] }
@ApiProperty({ enum: Priority })
priority: Priority;

// Both styles: { $ref: '#/components/schemas/Priority' }
@ApiProperty({ enum: Priority, enumName: 'Priority' })
priority: Priority;

extractValidation

Extract validation constraints from class-validator decorators (default: true):

options: {
  extractValidation: true,
}

See the Validation Guide for details.

excludeDecorators

Names of your own decorators that exclude endpoints from the spec. Controller and method decorators both count, and so do decorators wrapped inside them:

options: {
  excludeDecorators: ['Internal', 'AdminOnly'],
}

decorators

What your custom decorators mean, in terms of built-in ones. Wrappers written with applyDecorators() in your own code are followed automatically (see Custom Decorators), so this is only needed for decorators that cannot be followed, such as wrappers from a compiled library or with conditional logic.

A mapping is either a list of decorators, or a function of the arguments the decorator was called with. Use typeRef('Name') to refer to a class or enum:

import { defineConfig, typeRef } from 'nestjs-openapi';

export default defineConfig({
  // ...
  options: {
    decorators: {
      // @InternalPort() hides a controller
      InternalPort: [{ name: 'ApiExcludeController' }],

      // @FilterField(Dto?) is an optional nested property
      FilterField: ({ args: [type] }) => [
        {
          name: 'ApiPropertyOptional',
          args: [{ type: type ?? typeRef('FilterDto') }],
        },
      ],

      // @Paginated(Dto) documents a paged list response
      Paginated: ({ args: [itemType] }) => [
        { name: 'ApiOkResponse', args: [{ type: itemType, isArray: true }] },
      ],
    },
  },
});

In a mapping function, arguments are plain values: literals, arrays and objects as written, classes and enums as TypeRef, and undefined for anything that cannot be read statically.

include and deepScanRoutes

Which modules' controllers are documented, like the options of the same name in SwaggerModule.createDocument(). By default, every controller reachable from the entry module is documented.

options: {
  // Only controllers declared directly in ExternalApiModule
  include: ['ExternalApiModule'],

  // Also controllers of the modules ExternalApiModule imports
  // (one level, global modules excluded)
  deepScanRoutes: true,
}

schemaNameCollision

Schemas are components named after their types, but different declarations can share a name: two files each declaring their own AddressDto, or two libraries exporting a PluginStateDto. References always resolve to the declaration they point to; this option only decides how such schemas are named. Only declarations reached from the documented routes count, so an unused class with the same name is not a collision.

Declarations whose schemas are equal are not a collision: they share one component under their name, with no warning, in every mode. Key order, required order and enum value order don't count; descriptions, examples and every other keyword do, and so do the schemas they refer to. A generic is compared per instantiation: ChoiceDto<string> from two identical ChoiceDto<T> declarations is one ChoiceDto_string, and if two declarations give ResultDto<string> different bodies, only that instantiation collides. An enumName given different values in different places is a collision too; outside 'error', those values are written in place.

ValueBehavior
'inline'Default. Each colliding schema is written in place where it is used, with a warning per name.
'rename'Each colliding declaration gets its own component, named after its file (see below).
'error'Generation fails, listing every colliding name, the files declaring it, and where they differ.
functionReturns the component name for each colliding declaration. Names must be unique.

'rename' names a component <File>_<Type>: the declaring file's name without .ts and .dto, in PascalCase. If two colliding declarations come from files with the same name, parent folders are prepended until they differ:

DeclarationComponent
AddressDto in shipping/shipping.dto.tsShipping_AddressDto
AddressDto in orders/dto/get-order-summary.dto.tsGetOrderSummary_AddressDto
PluginStateDto in libs/a/plugin-state.dto.tsAPluginState_PluginStateDto
PluginStateDto in libs/b/plugin-state.dto.tsBPluginState_PluginStateDto

When only some declarations differ, equal ones share a group and are named after the first file. A function receives each colliding declaration; it is not called for names whose declarations are all equal:

options: {
  schemaNameCollision: ({ name, relativePath, exported, declarations }) =>
    `${relativePath.split('/')[1]}_${name}`,
}

relativePath is relative to the config file. If a function returns the same name twice, or the name of another schema, generation fails with a message naming both declarations.

pathFilter

Filter paths by regex or predicate function. Paths matching the regex (or returning true) are included:

options: {
  // Exclude paths containing '/internal/'
  pathFilter: /^(?!.*\/internal\/).*/,

  // Using a function
  pathFilter: (path) => !path.includes('/internal/'),

  // Include only v2 API paths
  pathFilter: /^\/api\/v2\//,
}

overlay

Applies an OpenAPI Overlay 1.0 document to the generated spec, before it is written. Give the overlay object, or the path of a JSON or YAML file, relative to the config file:

# public.overlay.yaml
overlay: 1.0.0
info:
  title: Public API
  version: 1.0.0
actions:
  - target: $.paths['/admin/users']
    description: Hide the admin routes
    remove: true
  - target: $.paths['/users'].get
    update:
      x-rate-limit: { perMinute: 100 }
options: {
  overlay: 'public.overlay.yaml',
}

Actions run in order, each on the result of the previous one. target is an RFC 9535 JSONPath. update is merged into each object it selects (nested objects merge, other values replace) or appended to each array it selects; remove: true deletes what it selects.

An action whose target matches nothing fails generation and names the action, so a renamed route cannot silently drop a change. Set overlayUnmatchedTarget: 'warn' to log a warning and skip it instead.

query

Query parameter handling options.

query.style

Controls how query DTOs are represented in the spec (default: "inline").

ValueDescription
"inline"Expand DTO properties as individual query parameters (default)
"ref"Keep as a single parameter with a schema reference

By default, when @Query() is used without an explicit parameter name (e.g., @Query() pagination: PaginationDto), the DTO properties are inlined as individual query parameters. This is the standard OpenAPI practice and works better with code generators and API clients.

// Controller:
@Get()
findAll(@Query() pagination: PaginationDto) { ... }

// Default output (style: "inline"):
// parameters:
//   - name: page
//     in: query
//     required: false
//   - name: limit
//     in: query
//     required: false

Set query.style: "ref" to keep query DTOs as schema references:

options: {
  query: { style: 'ref' },
}

// Output with style: "ref":
// parameters:
//   - name: pagination
//     in: query
//     schema:
//       $ref: '#/components/schemas/PaginationDto'

Explicit named params like @Query('filter') filter: FilterDto are never inlined—they always use a schema reference regardless of this setting.

schemas

Schema generation and normalization options.

order

Order of paths, operations, schema properties and component schemas (default: "source").

ValueOutput
"source"Declaration order, as @nestjs/swagger writes it. Paths follow the controllers as they are reached from the entry module, operations and methods follow the class, properties follow the class, and component schemas follow the order they are first reached (schemas nothing reaches come last, A to Z)
"alphabetical"Paths, operations, properties and component schemas sorted A to Z

Other object keys, such as the keys inside an operation, stay in one fixed order either way.

options: {
  order: 'alphabetical', // the order before 'source' became the default
}

schemas.aliasRefs

Controls how pass-through alias schemas are emitted (default: "collapse").

ValueDescription
"collapse"Rewrite alias chains like A -> B -> C to A -> C when B is only a $ref wrapper, then remove B
"preserve"Keep alias schemas in components.schemas
options: {
  schemas: {
    aliasRefs: 'collapse', // default
  },
}

schemas.genericNames

Names of generic instantiations (default: "sanitized"). OpenAPI only allows letters, digits, ., - and _ in component names.

ValueExample
"sanitized"Page<User> → Page_User, Page<User[]> → Page_UserArray, Result<A | B> → Result_A_Or_B
"raw"Page<User> as is; not valid OpenAPI, though some tools accept it

schemas.additionalProperties

Whether object schemas allow properties they do not list (default: "omit").

ValueOutput
"omit"No additionalProperties, as @nestjs/swagger writes it. A field added to a response later is not a breaking change
"forbid"additionalProperties: false on every component object that lists its properties, for clients that reject unknown fields

An additionalProperties declared in @ApiProperty, or an index signature such as Record<string, T>, is kept either way.

extends

extends takes the path of a base config, relative to the config that declares it. Values in the child override the base (objects merge, arrays are replaced), and a base can extend another base.

Each path in a config is relative to the file that declares it, the way tsconfig extends works. This covers output, files.entry, files.tsconfig, files.dtoGlob (negated and brace patterns too) and options.overlay given as a path. A base config in another folder keeps pointing at its own files, and a child that sets one of these fields resolves it against its own folder.

export default defineConfig({
  extends: '../shared/base.config.ts',
  output: 'openapi.json',
  openapi: { info: { title: 'My API', version: '2.0.0' } },
});

Complete Example

import {  } from 'nestjs-openapi';

export default ({
  : 'src/openapi/openapi.generated.json',
  : 'json',

  : {
    : 'src/app.module.ts',
    : 'tsconfig.json',
    : 'src/**/*.dto.ts',
    : ['**/*.spec.ts', '**/*.test.ts'],
  },

  : {
    : {
      : 'Todo API',
      : '1.0.0',
      : 'A simple todo management API',
      : {
        : 'API Support',
        : 'support@example.com',
      },
      : {
        : 'MIT',
      },
    },
    : [
      { : 'https://api.todo.example.com', : 'Production' },
      { : 'http://localhost:3000', : 'Development' },
    ],
    : [
      { : 'todos', : 'Todo operations' },
      { : 'users', : 'User management' },
    ],
    : {
      : [
        {
          : 'bearerAuth',
          : 'http',
          : 'bearer',
          : 'JWT',
        },
      ],
      : [{ : [] }],
    },
  },

  : {
    : '/api/v1',
    : true,
    : ['Internal'],
    : /^(?!.*\/internal\/).*/,
  },
});

Next Steps

On this page