NestJS Drizzle Swagger docs for lists

ApiCrudList documents NestJS Drizzle list routes in Swagger. It covers paging, sort, cursor, search, relations and every filter column from your allowlists.

For NestJS Drizzle Swagger docs, put @ApiCrudList(source) on a list route. It documents every query parameter parseQuery reads (page, limit, sort, cursor, withCount, search, fields, relations, counts) plus one parameter per filterable column with its type, and it adds the page envelope as the 200 response. @ApiCrudQuery(source) adds the parameters alone. Both read the same table and allowlists the service uses.

@nestjs/swagger is optional

@nestjs/swagger is an optional peer dependency. The package loads it lazily, the first time one of these decorators is applied, so an app that never uses them never loads it. If it is not installed, applying a decorator throws an error that tells you to install it.

How do I document a list route?

Install @nestjs/swagger

pnpm add @nestjs/swagger

Share the entity config

Put the allowlists and relations in one constant, so forFeature and the decorator read the same facts:

countries.crud.ts
import type { CrudFeatureConfig } from '@avytrix/nestjs-drizzle-crud';

export const countriesCrud = {
  filterableColumns: ['name', 'code', 'region.name'],
  sortableColumns: ['name', 'code'],
  searchableColumns: ['name'],
} satisfies CrudFeatureConfig;

Spread it into the module: DrizzleCrudModule.forFeature([{ service: CountriesService, table: countries, config: countriesCrud }]).

Decorate the route

countries.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { ApiCrudList } from '@avytrix/nestjs-drizzle-crud';
import { countries } from '../db/schema';
import { countriesCrud } from './countries.crud';
import { CountriesService } from './countries.service';

@Controller('countries')
export class CountriesController {
  constructor(private readonly countries: CountriesService) {}

  @Get()
  @ApiCrudList({ table: countries, ...countriesCrud })
  findAll(@Query() query: Record<string, unknown>) {
    const q = this.countries.parseQuery(query);
    return q.isCursor
      ? this.countries.findByCursor(q.filters, q.pagination, q.options)
      : this.countries.findAll(q.filters, q.pagination, q.options);
  }
}

Serve the document

main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

SwaggerModule.setup('docs', app, () =>
  SwaggerModule.createDocument(
    app,
    new DocumentBuilder().setTitle('My API').setVersion('1.0').build(),
  ),
);

The UI is at /docs and the JSON at /docs-json.

What do I pass as the source?

The table plus the same allowlists the service uses, so the docs match what the API accepts. There are two shapes:

  • A config object with the table: { table, relations, filterableColumns, sortableColumns, selectableColumns, searchableColumns, loadableRelations, allowTrash }. Every key except table is optional. This is the usual pattern, with the shared constant spread in.
  • A service class decorated with @CrudService({ table, ... }), whose metadata already holds the table and the lists. A class without that metadata throws an error that names what to pass instead.

Config set only in forFeature is not visible when the decorator runs, because decorators run when the class is defined. That is why the shared constant matters.

Which decorator do I use?

ApiCrudList(source, options?)Optional

MethodDecorator

Every query parameter, plus the 200 response with the page envelope and the row schema.
ApiCrudQuery(source, options?)Optional

MethodDecorator

The query parameters only. Use it when you document the response yourself.
options.cursorOptional

boolean

Document cursor and count. Set false on a route that only pages by offset.

Defaults to true

options.trashOptional

boolean

The route lists the trash: leave out withDeleted and onlyDeleted.

Defaults to false

What gets documented?

Each parameter appears only when the entity can use it:

ParameterDocumented when
page, limit, sortBy, sortOrder, withCount, fieldsalways; sortBy and fields list the sortable and selectable columns
with, countsthe entity has relations; with lists the loadable ones
searchsearchColumns or searchableColumns is set
cursor, countunless { cursor: false }
withDeleted, onlyDeletedallowTrash: true, and not on a trash route ({ trash: true })
one parameter per filter columnevery entry of filterableColumns, or every column when it is unset

A relation path is written the way a query string sends it: region.name becomes region[name]. Each filter parameter carries the column's OpenAPI type (integer, number, boolean, date-time, uuid, int64 for a bigint, an enum's values) and lists the bracket operators, such as name[startsWith]=In. Every parameter is optional.

ApiCrudList also adds the 200 response: { data, total, page, limit, hasMore, nextCursor }. The row schema comes from the table's columns, nullable columns are marked nullable, and total is nullable because withCount=false skips the count.

Does the controller factory document itself?

Yes, through the openapi option of createCrudController. It applies ApiCrudList(source) to the list route and ApiCrudList(source, { trash: true }) to the trash route:

projects.controller.ts
@Controller('projects')
export class ProjectsController extends createCrudController(ProjectsService, {
  openapi: { table: projects, ...projectsCrud },
}) {}

Pass openapi: true instead when the service carries @CrudService metadata.

Common questions

Next steps

Last updated View change history

On this page