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.

Before — handwritten
@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) };
}
After — nestjs-rest-query
// 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.

AdapterStatusSource factory
TypeORMStabletypeormSource(repository) — from nestjs-rest-query/typeorm. Reference adapter.
PrismaStableprismaSource({ client, model, manifest }) — from nestjs-rest-query/prisma. Pattern operators are refused on SQL Server and SQLite.
DrizzleStabledrizzleSource({ db, dialect, table, relations }) — from nestjs-rest-query/drizzle. Requires drizzle-orm 1.x; 0.45.x is not accepted.

Quickstart

  1. 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
  2. 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 {}
  3. 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'] },
        },
      },
    );