NestJS · TypeORM · Prisma · Drizzle
Turn REST query strings into safe database queries.
nestjs-rest-query parses filters, sorts, pagination, field selection, relation loading and text search into a typed query plan, authorises that plan against a per-endpoint whitelist, and lets an ORM adapter compile it. Three adapters, one semantic core, one answer to the same request.
From handwritten query plumbing to two declarations
A schema says what the model is — kinds, nullability, relations. Endpoint rules say what this route authorises — exact paths, operators per field, projections. The compiled rules are the single source for both runtime authorisation and the generated OpenAPI parameters, so the two cannot drift apart.
@Get()
async listCompanies(@Query() query: ListCompaniesQuery) {
const qb = this.companies.createQueryBuilder('company');
if (query.name) qb.andWhere('company.name ILIKE :n', { n: `%${query.name}%` });
if (query.cnpj) qb.andWhere('company.cnpj = :c', { c: query.cnpj });
if (query.createdFrom) qb.andWhere('company.createdAt >= :f', { f: query.createdFrom });
if (query.sort === 'name') qb.orderBy('company.name', query.dir ?? 'ASC');
if (query.sort === 'createdAt') qb.orderBy('company.createdAt', query.dir ?? 'DESC');
const page = Number(query.page ?? 1);
const perPage = Math.min(Number(query.perPage ?? 20), 100);
qb.skip((page - 1) * perPage).take(perPage);
const [data, total] = await qb.getManyAndCount();
return { data, page, perPage, total, lastPage: Math.ceil(total / perPage) };
}// company.query.ts — declared once, outside the controller
export const companyRules = defineQueryRules(COMPANY_SCHEMAS, 'company', {
filters: [
{ path: 'name', operators: ['eq', 'ilike'] },
{ path: 'cnpj', operators: ['eq', 'in'] },
{ path: 'createdAt', operators: ['gte', 'lt', 'between'] },
],
sorts: ['name', 'createdAt'],
fields: {
root: {
allowed: ['id', 'name', 'cnpj', 'createdAt'],
default: ['id', 'name', 'cnpj', 'createdAt'],
},
},
});
// company.controller.ts
@Get()
@ApiDynamicQuery(companyRules)
findAll(
@Query() query: DynamicQueryDto,
@QueryRules() rules: CompiledQueryRules,
) {
return this.qb.execute(typeormSource(this.companies), query, rules);
}Adapter compatibility
All three adapters compile the same query plan and are measured by the same parity corpus across PostgreSQL, MySQL and SQL Server. The adapter is not a global setting — it comes from the source you hand to execute(), and each source factory lives in its own subpath.
| Adapter | Status | Source factory |
|---|---|---|
| TypeORM | Stable | typeormSource(repository) — from nestjs-rest-query/typeorm. Reference adapter. |
| Prisma | Stable | prismaSource({ client, model, manifest }) — from nestjs-rest-query/prisma. Pattern operators are refused on SQL Server and SQLite. |
| Drizzle | Stable | drizzleSource({ db, dialect, table, relations }) — from nestjs-rest-query/drizzle. Requires drizzle-orm 1.x; 0.45.x is not accepted. |
Quickstart
- 1Install
Add the package, then the one ORM you use. Every ORM peer is optional and the root entrypoint loads none of them.
pnpm add nestjs-rest-query pnpm add typeorm @nestjs/typeorm - 2Register the module
Import DynamicQueryBuilderModule once, in your AppModule. No adapter goes here — forRoot configures common policy only.
import { DynamicQueryBuilderModule } from 'nestjs-rest-query'; @Module({ imports: [ DynamicQueryBuilderModule.forRoot({ pagination: { defaultPerPage: 10, maxPerPage: 100 }, }), ], }) export class AppModule {} - 3Declare the schema and the rules
The schema describes the model; the rules describe what this endpoint authorises. The whitelist is exact — authorising company does not authorise company.name.
export const companyRules = defineQueryRules( COMPANY_SCHEMAS, 'company', { filters: [{ path: 'name', operators: ['eq', 'ilike'] }], sorts: ['name', 'createdAt'], fields: { root: { allowed: ['id', 'name'], default: ['id', 'name'] }, }, }, );