Aviary
Guides

Filter Classes

Writing filter classes — validated mode, lean mode, setup(), $query, $input, $context, and advanced features.

A filter class is a regular NestJS injectable that extends an ORM-specific base class (MikroOrmFilter<E> or TypeOrmFilter<E>). Methods decorated with @FilterFor() each handle one input key, mutating the query builder to add constraints.


Validated mode (filter as DTO)

When you add class-validator decorators to your filter class, it acts as both the filter and the validation DTO. Input is validated before any filter method runs.

import { Injectable } from '@nestjs/common';
import { Filterable, FilterFor, escapeLike } from '@dudousxd/nestjs-filter';
import { MikroOrmFilter } from '@dudousxd/nestjs-filter-mikro-orm';
import { IsOptional, IsString, IsNumber, Min } from 'class-validator';
import { Type } from 'class-transformer';
import { User } from './user.entity.js';

@Injectable()
@Filterable({ entity: User })
export class UserFilter extends MikroOrmFilter<User> {
  @IsOptional() @IsString()
  name?: string;

  @IsOptional() @IsNumber() @Min(0)
  @Type(() => Number)
  minAge?: number;

  @FilterFor('name')
  applyName(value: string) {
    this.whereLike('name', value);
  }

  @FilterFor('minAge')
  applyMinAge(value: number) {
    this.$query.andWhere({ age: { $gte: value } });
  }
}

Use @Type(() => Number) from class-transformer on numeric fields. Query strings always arrive as strings, and class-transformer handles the coercion before validation runs.


Lean mode (no validation)

If you skip the class-validator decorators, the filter works in lean mode. Input keys are dispatched to matching @FilterFor() methods without validation.

@Injectable()
@Filterable({ entity: User })
export class UserFilter extends MikroOrmFilter<User> {
  @FilterFor('name')
  applyName(value: string) {
    this.whereLike('name', value);
  }

  @FilterFor('minAge')
  applyMinAge(value: number) {
    this.$query.andWhere({ age: { $gte: value } });
  }
}

You can also explicitly disable validation by setting validation: 'off' in FilterModule.forRoot().


Auto-fields

By default, @Filterable({ entity: User }) enables auto-fields — any input key matching an entity column is automatically applied as a WHERE condition. Dot-notation for relations (posts.title) also works automatically. This means you can write zero-method filter classes for simple equality, array, and operator-based queries.

To opt out: @Filterable({ entity: User, autoFields: false })

Default behavior (auto-fields on)

@Injectable()
@Filterable({ entity: User })
export class UserFilter extends MikroOrmFilter<User> {
  // No @FilterFor methods needed for simple equality checks.
  // GET /users?name=Al&status=active  →  WHERE name = 'Al' AND status = 'active'
  // GET /users?status[]=A&status[]=B  →  WHERE status IN ('A', 'B')
}

The adapter introspects entity metadata (via getEntityFields()) to discover valid columns and rejects unknown fields automatically. This prevents unknown-column probing and SQL errors without requiring an explicit allowed list.

You can restrict which fields are auto-applied by combining with allowed:

@Filterable({ entity: User, allowed: ['name', 'email', 'status', 'age'] })

The adapter introspects the entity's ORM metadata to build the set of valid column names. Only keys matching real entity columns are accepted; all others are silently skipped. If the adapter does not support metadata introspection, it falls back to accepting all keys (legacy behavior) with a logged warning.

If your adapter does not implement getEntityFields(), using auto-fields without an allowed list means any column name the client sends will be accepted. Always combine with allowed to restrict which fields are queryable, or ensure your adapter supports metadata introspection.

Explicit field list

You can also pass an explicit array of field names to auto-apply:

@Filterable({ entity: User, autoFields: ['name', 'email'] })

Only name and email will be auto-applied. Other keys still need @FilterFor methods.

Opting out

To disable auto-fields entirely, set autoFields: false:

@Filterable({ entity: User, autoFields: false })

With this setting, only keys with @FilterFor methods are dispatched. Unknown keys are handled by the onUnknownKey policy.

Mixing auto-fields with @FilterFor

You can combine auto-fields with explicit @FilterFor methods. Any key that has a @FilterFor mapping is dispatched to the method; only unmatched keys fall through to the auto-field handler.

@Injectable()
@Filterable({ entity: User, allowed: ['name', 'status', 'role'] })
export class UserFilter extends MikroOrmFilter<User> {
  @FilterFor('name')
  applyName(value: string) {
    // Custom LIKE behavior for name
    this.whereLike('name', value);
  }

  // 'status' and 'role' are auto-applied with simple equality
}

Bracket notation with auto-fields

Auto-fields support operator objects via query string bracket notation:

GET /users?name[contains]=fleet&age[gte]=18&age[lte]=65

The adapter detects operator objects and applies them accordingly (e.g. LIKE '%fleet%' for contains, >= 18 for gte).


The @FilterFor() decorator

@FilterFor(inputKey?) maps an input key to the decorated method. If inputKey is omitted, the method name is used as the key.

// Explicit key mapping
@FilterFor('name')
applyName(value: string) { /* ... */ }

// Method name = key (equivalent to @FilterFor('status'))
@FilterFor()
status(value: string) { /* ... */ }

Each filter method receives two arguments:

  • value -- the input value for this key
  • key -- the input key name (useful when one method handles multiple keys)

The setup() hook

The optional setup() method runs once before any filter methods are dispatched. Use it for pre-processing that applies regardless of which input keys are present.

@Injectable()
@Filterable({ entity: Order })
export class OrderFilter extends MikroOrmFilter<Order> {
  setup() {
    // Always filter to the current user's orders
    const userId = this.$context.user?.id;
    if (userId) {
      this.$query.andWhere({ userId });
    }
  }

  @FilterFor('status')
  applyStatus(value: string) {
    this.$query.andWhere({ status: value });
  }
}

setup() can be synchronous or async. If it throws, the error is wrapped in a FilterMethodException with key === 'setup'.


Accessing $query, $input, and $context

Inside any filter method (including setup()), three properties are available via AsyncLocalStorage:

PropertyTypeDescription
this.$queryORM QueryBuilderThe query builder being built up. MikroORM: QueryBuilder<E>. TypeORM: SelectQueryBuilder<E>.
this.$inputReadonly<Record<string, unknown>>The full normalized input object (frozen).
this.$contextFilterContextOptional context: { req?, user?, raw? }. Set by the interceptor or FilterRunner.apply().
this.$adapterFilterAdapter | nullThe active ORM adapter, if registered.

The input() helper method

The input() method provides convenient access to input values:

// Get full input object
const all = this.input();

// Get a single key
const name = this.input('name');

// Get with default value
const page = this.input('page', 1);

whitelistMethod() and blacklistMethod()

These protected methods let you dynamically allow or block filter keys at runtime, typically called in setup():

setup() {
  const isAdmin = this.$context.user?.role === 'admin';
  if (!isAdmin) {
    // Non-admins cannot filter by internal status
    this.blacklistMethod('internalStatus');
  }
}
setup() {
  // Dynamically allow a key that is not in the static @FilterFor map
  if (this.input('enableAdvanced')) {
    this.whitelistMethod('advancedScore');
  }
}

Whitelisted keys bypass the static allowed/blocked checks in the dispatcher. Blacklisted keys are skipped regardless of configuration.


push() for dynamic input injection

The push() method injects additional key/value pairs into the dispatch queue. Pushed entries are processed after the current dispatch loop completes (BFS order).

@FilterFor('dateRange')
applyDateRange(value: string) {
  const [start, end] = value.split(',');
  this.push('startDate', start);
  this.push('endDate', end);
}

@FilterFor('startDate')
applyStartDate(value: string) {
  this.$query.andWhere({ createdAt: { $gte: new Date(value) } });
}

@FilterFor('endDate')
applyEndDate(value: string) {
  this.$query.andWhere({ createdAt: { $lte: new Date(value) } });
}

You can also push multiple keys at once:

this.push({ startDate: '2024-01-01', endDate: '2024-12-31' });

The related() method lets you filter by a related entity's columns imperatively, without needing the @Relations decorator:

@FilterFor('publishedOnly')
async applyPublishedOnly(value: boolean) {
  if (value) {
    await this.related('posts', { status: 'published' });
  }
}

This creates a join on the posts relation and adds WHERE posts.status = 'published'. It delegates to the adapter's applyRelationConstraint under the hood.

// Full signature
await this.related(relationName: string, conditions: Record<string, unknown>): Promise<void>

Use related() when you need conditional or dynamic relation filtering inside a @FilterFor method. For static key-to-relation mapping, prefer the @Relations decorator.


Inheritance

@FilterFor metadata is inherited from parent classes. A child class can override or extend parent filter methods:

@Injectable()
@Filterable({ entity: User })
export class BaseUserFilter extends MikroOrmFilter<User> {
  @FilterFor('name')
  applyName(value: string) {
    this.whereLike('name', value);
  }
}

@Injectable()
@Filterable({ entity: User })
export class AdminUserFilter extends BaseUserFilter {
  // Inherits applyName from BaseUserFilter

  @FilterFor('role')
  applyRole(value: string) {
    this.$query.andWhere({ role: value });
  }
}

The getFilterForMap() function walks the prototype chain, so parent @FilterFor mappings are available in child classes. If a child defines the same input key, the child's method takes precedence.


@Filterable() options

The @Filterable() class decorator associates the filter with an entity and optionally restricts which input keys are dispatched:

@Filterable({
  entity: User,
  allowed: ['name', 'email', 'status'],  // only these keys
})
@Filterable({
  entity: User,
  blocked: ['internalId', 'secret'],     // everything except these
})
OptionTypeDescription
entityType<unknown>Required. The entity class this filter targets.
allowedreadonly string[]Whitelist of input keys. Only these keys are dispatched.
blockedreadonly string[]Blacklist of input keys. These keys are never dispatched.

You cannot specify both allowed and blocked on the same filter. The decorator throws an error if both are set.


ORM helper methods

Both MikroOrmFilter and TypeOrmFilter provide convenience methods for common LIKE queries:

MethodSQL equivalent
this.whereLike(field, value)field LIKE '%value%'
this.whereBeginsWith(field, value)field LIKE 'value%'
this.whereEndsWith(field, value)field LIKE '%value'

All values are escaped via escapeLike() to prevent SQL injection through LIKE wildcards.

@FilterFor('name')
applyName(value: string) {
  this.whereLike('name', value); // safe: escapes %, _, and \
}

The escapeLike() function is also exported from @dudousxd/nestjs-filter for manual use:

import { escapeLike } from '@dudousxd/nestjs-filter';

const safe = escapeLike(userInput); // escapes %, _, \

On this page