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.
createCrudController(Service, options) is the NestJS CRUD generator for Drizzle in this package: it returns a controller class whose routes call your service. You get offset and cursor lists, get, create, update, delete, soft delete, restore, a trash list and four opt-in bulk routes. The id pipe follows the primary key column, so you stop copying the same controller for every table.
Before you start
You need a service registered with forFeature. Every route goes through
that service, so allowlists, scope, soft delete,
optimistic locking and events apply exactly as they
do when you call the service in code.
How do I generate a controller?
Extend the class the factory returns and put @Controller on your subclass:
import { Controller } from '@nestjs/common';
import { createCrudController } from '@avytrix/nestjs-drizzle-crud';
import { CreateProjectDto } from './dto/create-project.dto';
import { UpdateProjectDto } from './dto/update-project.dto';
import { ProjectsService } from './projects.service';
@Controller('projects')
export class ProjectsController extends createCrudController(ProjectsService, {
dto: { create: CreateProjectDto, update: UpdateProjectDto },
deleteMode: 'soft',
routes: { bulkCreate: true, bulkDelete: true },
}) {}Import the DTOs as values, not with import type. The factory hands the classes to Nest, and a type-only import is erased at build time.
Which routes does it generate?
Eight routes are on by default. The four bulk routes stay off until you turn them on.
| Route | Method and path | Calls | Status | Default |
|---|---|---|---|---|
list | GET / | findAll, or findByCursor when ?cursor= (or ?after=) is present | 200 | on |
trash | GET /trash | findAll or findByCursor with onlyDeleted | 200 | on |
get | GET /:id | find, 404 when the row is missing | 200 | on |
create | POST / | create | 201 | on |
update | PATCH /:id | update | 200 | on |
delete | DELETE /:id | delete, or softDelete with deleteMode: 'soft' | 200 | on |
softDelete | POST /:id/soft-delete | softDelete | 200 | on |
restore | POST /:id/restore | restore | 200 | on |
bulkCreate | POST /bulk | massCreate, body is an array | 201 | off |
bulkUpdate | PATCH /bulk | massUpdate, body is { ids, data } | 200 | off |
bulkDelete | POST /bulk-delete | massDelete, or massSoftDelete with deleteMode: 'soft', body is { ids } | 200 | off |
bulkRestore | POST /bulk-restore | massRestore, body is { ids } | 200 | off |
A few rules hold across the table:
- Query strings.
listandtrashrun the whole query string throughparseQuery, so filters,sortBy,fields,with,counts,withCountandcursorall work.get,create,update,restoreand the bulk writes read only?with=,?fields=and?counts=and ignore the rest. - Trash. On an entity without soft delete,
trash,softDelete,restoreandbulkRestoreanswer 404 withThis resource has no trash. The trash route turnsallowTrashoff, so?withDeleted=cannot widen it. - Bulk bodies.
idsmust be a non-empty array anddatamust be an object. Anything else is a 400. - Route order. The static paths (
/trash,/bulk,/bulk-delete,/bulk-restore) are registered before/:id, so they never match as an id.
How is the id parsed?
From the primary key column at request time, the way the matching Nest pipe would parse it:
| Primary key column | Parsing | Bad input |
|---|---|---|
| integer, serial | ParseIntPipe | 400 Validation failed (numeric string is expected) |
| uuid | ParseUUIDPipe | 400 |
| bigint in bigint mode | digits (with an optional leading -), then BigInt | 400 |
| anything else, including numeric and decimal | the string as is | none |
The service exposes the same logic as service.parseId(raw) and service.idKind(), which you can call from your own controllers. Set idType: 'int' | 'uuid' | 'string' to fix the pipe when the class is built instead. It then shows on the route as a normal @Param('id', ParseIntPipe).
Which options does createCrudController take?
Every option is optional. The source is crud-controller.factory.ts.
pathOptionalstring
idTypeOptional"int" | "uuid" | "string"
dto.createOptionalType
dto.updateOptionalType
queryOptionalParseCrudQueryOptions
Defaults to {}
routesOptional{ [route: CrudRouteName]: boolean | CrudRouteOptions }
decoratorsOptionalMethodDecorator[]
classDecoratorsOptionalClassDecorator[]
deleteModeOptional"hard" | "soft"
Defaults to "hard"
openapiOptionaltrue | ApiCrudSource
How do I validate request bodies?
Pass the DTO classes in dto. They become the parameter types of create and update, so a global ValidationPipe validates them the usual way. bulkCreate validates each array element with ParseArrayPipe({ items: dto.create }).
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));Without dto, bodies reach the service unvalidated. writableColumns still applies, so a body cannot write a column you did not allow.
How do I turn routes on or off?
Set a route to true or false in routes. An object turns it on and carries decorators for that route alone:
@Controller('tags')
export class TagsController extends createCrudController(TagsService, {
routes: {
delete: false, // no DELETE /:id
bulkDelete: true, // POST /bulk-delete
update: { decorators: [Roles('admin')] }, // PATCH /:id, admins only
},
}) {}Roles stands for your own role decorator. { enabled: false } works the same as false.
How do I add guards, interceptors or Swagger tags?
decorators applies method decorators to every route, before the per-route ones. classDecorators decorates the generated class. You can also decorate your subclass directly, which reads like any other Nest controller:
@ApiTags('projects')
@UseGuards(AuthGuard)
@Controller('projects')
export class ProjectsController extends createCrudController(ProjectsService, {
decorators: [UseInterceptors(AuditInterceptor)],
}) {}How do I document the routes in Swagger?
Pass openapi. The factory applies ApiCrudList to list, and ApiCrudList(source, { trash: true }) to trash:
@Controller('projects')
export class ProjectsController extends createCrudController(ProjectsService, {
openapi: { table: projects, ...projectsCrud },
}) {}Keep the entity config in one constant (projectsCrud) and spread it into both forFeature and openapi. The docs then list the same filterable and sortable columns the service accepts.
Can I add my own routes?
Yes. The factory returns a class, so extend it and add methods. The service is on this.service:
@Controller('projects')
export class ProjectsController extends createCrudController(ProjectsService) {
@Get('stats/by-name')
stats() {
return this.service.countBy('name');
}
}To replace a generated route, turn it off in routes and declare your own handler for the same path. Do not override a generated method by name: the route metadata sits on the generated handler, so Nest would not route your override.
Common questions
No. bulkCreate, bulkUpdate, bulkDelete and bulkRestore are off until you set them to true in routes. The other eight routes are on.
It deletes the row unless you pass deleteMode: 'soft', which makes it call softDelete instead. POST /:id/soft-delete always soft deletes.
Only when you pass dto and run a global ValidationPipe. Without DTOs the body goes straight to the service, where writableColumns still applies.
The entity has soft delete turned off. The trash, soft delete and restore routes answer 404 with This resource has no trash in that case.
Extend the generated class and add a decorated method. The service is available as this.service.
Next steps
OpenAPI docs for list queries
Document every query parameter and the page envelope in Swagger.
Parse query strings
What the list route accepts in its query string.
Multi-tenancy
Scope every generated route to the tenant of the request.
Need a team?
Avytrix Technologies maintains this library and builds NestJS APIs for clients.
Error handling in NestJS with Drizzle
NestJS Drizzle error handling without a custom filter. Each exception extends a Nest HTTP exception, so a missing row is a 404 and a duplicate a 409.
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.