Using Filters in Services
How to apply filters programmatically in services using @ApplyFilter (controller) and FilterRunner.apply() (service).
nestjs-filter provides two primary APIs for applying filters:
@ApplyFilter-- parameter decorator for controllersFilterRunner.apply()-- programmatic use in services
There is no need for repository wrapper classes. You create a QueryBuilder from your ORM, pass it to FilterRunner.apply(), and continue chaining or executing as usual.
Controller: @ApplyFilter
The @ApplyFilter parameter decorator resolves input from the request, runs the filter, and injects the resulting QueryBuilder directly:
import { Controller, Get } from '@nestjs/common';
import { ApplyFilter } from '@dudousxd/nestjs-filter';
import type { QueryBuilder } from '@mikro-orm/sql';
import { User } from './user.entity.js';
import { UserFilter } from './user.filter.js';
@Controller('users')
export class UsersController {
@Get()
list(@ApplyFilter(UserFilter) qb: QueryBuilder<User>) {
return qb.getResultList();
}
}import { Controller, Get } from '@nestjs/common';
import { ApplyFilter } from '@dudousxd/nestjs-filter';
import type { SelectQueryBuilder } from 'typeorm';
import { User } from './user.entity.js';
import { UserFilter } from './user.filter.js';
@Controller('users')
export class UsersController {
@Get()
list(@ApplyFilter(UserFilter) qb: SelectQueryBuilder<User>) {
return qb.getMany();
}
}Service: FilterRunner.apply()
For programmatic use in services, inject FilterRunner and call .apply() with a QueryBuilder you create yourself:
import { Injectable } from '@nestjs/common';
import { FilterRunner } from '@dudousxd/nestjs-filter';
import type { SqlEntityManager } from '@mikro-orm/sql';
import { User } from './user.entity.js';
import { UserFilter } from './user.filter.js';
@Injectable()
export class UsersService {
constructor(
private readonly runner: FilterRunner,
private readonly em: SqlEntityManager,
) {}
async search(input: Record<string, unknown>) {
const qb = this.em.createQueryBuilder(User);
await this.runner.apply(UserFilter, input, qb);
return qb.getResultList();
}
}import { Injectable } from '@nestjs/common';
import { FilterRunner } from '@dudousxd/nestjs-filter';
import { DataSource } from 'typeorm';
import { User } from './user.entity.js';
import { UserFilter } from './user.filter.js';
@Injectable()
export class UsersService {
constructor(
private readonly runner: FilterRunner,
private readonly dataSource: DataSource,
) {}
async search(input: Record<string, unknown>) {
const qb = this.dataSource.getRepository(User).createQueryBuilder('user');
await this.runner.apply(UserFilter, input, qb);
return qb.getMany();
}
}This pattern aligns with Spring Boot's Specification approach -- filters are applied to query builders directly, without intermediate repository wrappers.
Injecting a concrete filter class
A filter class registered with FilterModule.forFeature([...]) is a normal NestJS provider, so you can inject it wherever you need it. In practice you rarely need to — FilterRunner.apply() resolves the class from the DI container for you (see the service example above), so you only pass the class reference. Passing the class token is all apply() needs:
import { Injectable } from '@nestjs/common';
import { FilterRunner } from '@dudousxd/nestjs-filter';
import type { SqlEntityManager } from '@mikro-orm/sql';
import { User } from './user.entity.js';
import { UserFilter } from './user.filter.js';
@Injectable()
export class UsersService {
constructor(
private readonly runner: FilterRunner,
private readonly em: SqlEntityManager,
) {}
async search(input: Record<string, unknown>) {
const qb = this.em.createQueryBuilder(User);
// `UserFilter` is resolved from the container internally — you never
// `new` it or look it up yourself.
await this.runner.apply(UserFilter, input, qb);
return qb.getResultList();
}
}Generic, table-driven filtering (no filter class)
When you want to filter any entity without writing a bespoke filter class — an
admin console over many tables, for example — use FilterRunner.applyDynamic().
It reads the entity's ORM metadata (via the adapter) to auto-derive filterable
columns, includes, search columns, sort, and pagination. There are no
@FilterFor methods, no setup() hook, and no whitelist/blacklist — only what
the entity's own schema exposes.
import { Injectable } from '@nestjs/common';
import type { Type } from '@nestjs/common';
import { FilterRunner } from '@dudousxd/nestjs-filter';
import type { SqlEntityManager } from '@mikro-orm/sql';
@Injectable()
export class AdminQueryService {
constructor(
private readonly runner: FilterRunner,
private readonly em: SqlEntityManager,
) {}
// Filter any entity purely from its ORM metadata — no filter class needed.
async query<E extends object>(entity: Type<E>, input: Record<string, unknown>) {
const qb = this.em.createQueryBuilder(entity);
await this.runner.applyDynamic(entity, input, qb);
return qb.getResultList();
}
}For a paginated result plus a total count in one call, use findAndCount(),
which owns query execution and loads relations pagination-safely (to-one on
the join, to-many in a follow-up query so limit/offset stay correct):
async page<E extends object>(entity: Type<E>, input: Record<string, unknown>) {
const { rows, total } = await this.runner.findAndCount(entity, input);
return { data: rows, total };
}Both
applyDynamic()andfindAndCount()validate every field against the entity's real columns — unknownwherecolumns, sorts, and select fields are dropped (or rejected whenthrowOnInvalidis set), so a malformed client query can never reach the ORM as an invalid column.
For keyset/cursor pagination over the same dynamic path, see
Cursor pagination (FilterRunner.findPage()).
Controller Integration
Using @ApplyFilter in controllers — method-aware source resolution, custom sources, dynamic filter selection, and error handling.
Cursor Pagination
Stable keyset (seek) pagination with FilterRunner.findPage() — opaque before/after cursors, multi-column sort with a primary-key tiebreaker.