nestjs-filter
Guides

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:

  1. @ApplyFilter -- parameter decorator for controllers
  2. FilterRunner.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() and findAndCount() validate every field against the entity's real columns — unknown where columns, sorts, and select fields are dropped (or rejected when throwOnInvalid is 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()).

On this page