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/swaggerShare the entity config
Put the allowlists and relations in one constant, so forFeature and the decorator read the same facts:
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
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
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 excepttableis 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?)OptionalMethodDecorator
ApiCrudQuery(source, options?)OptionalMethodDecorator
options.cursorOptionalboolean
Defaults to true
options.trashOptionalboolean
Defaults to false
What gets documented?
Each parameter appears only when the entity can use it:
| Parameter | Documented when |
|---|---|
page, limit, sortBy, sortOrder, withCount, fields | always; sortBy and fields list the sortable and selectable columns |
with, counts | the entity has relations; with lists the loadable ones |
search | searchColumns or searchableColumns is set |
cursor, count | unless { cursor: false } |
withDeleted, onlyDeleted | allowTrash: true, and not on a trash route ({ trash: true }) |
| one parameter per filter column | every 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:
@Controller('projects')
export class ProjectsController extends createCrudController(ProjectsService, {
openapi: { table: projects, ...projectsCrud },
}) {}Pass openapi: true instead when the service carries @CrudService metadata.
Common questions
No. It is an optional peer dependency, loaded only when you apply ApiCrudQuery or ApiCrudList. Without the decorators you have nothing extra to install.
The decorator runs when the class is defined and cannot see forFeature config. Keep the config in one constant and spread it into both forFeature and the decorator.
The way a query string sends it: the allowlist entry region.name is documented as the parameter region[name].
ApiCrudQuery documents the query parameters only. ApiCrudList adds the 200 response with the page envelope and the row schema.
Next steps
NestJS CRUD controller generator
Generate the routes and document them with one option.
Parse query strings
What each documented parameter does at runtime.
Allowlists
Choose which columns callers may filter, sort, select and search.
NestJS CRUD controller generator
createCrudController is a NestJS CRUD generator for Drizzle. One call builds list, get, create, update, delete, trash and bulk routes from your service.
NestJS Drizzle module forRoot options
Configure the NestJS Drizzle module with forRoot. Set the connection, dialect, soft delete, timestamps, pagination and SQL defaults once in AppModule.