Aviary
Guides

Generic Operators

Operator-based filtering with ColumnFilter, 22 operators, query string bracket notation, body format, AND/OR composition, and security.

nestjs-filter supports generic operator-based filtering alongside the declarative @FilterFor pattern. Instead of writing a method for every field, you can send structured filter conditions with operators like equals, contains, in, between, and 18 more.


ColumnFilter shape

A single filter condition is represented as a ColumnFilter object:

interface ColumnFilter {
  field: string;                // column name (e.g. 'name', 'age')
  operator: FilterOperatorInput; // one of 22 operators (or a symbol alias like '=')
  value?: unknown;              // the filter value (omitted for unary operators)
  AND?: ColumnFilter[];         // nested AND conditions
  OR?: ColumnFilter[];          // nested OR conditions
}

All 22 operators

Comparison operators

OperatorSQL equivalentValue typeExample
equals= valuescalar{ field: 'status', operator: 'equals', value: 'active' }
notEquals!= valuescalar{ field: 'status', operator: 'notEquals', value: 'deleted' }
gt> valuescalar{ field: 'age', operator: 'gt', value: 18 }
gte>= valuescalar{ field: 'age', operator: 'gte', value: 18 }
lt< valuescalar{ field: 'age', operator: 'lt', value: 65 }
lte<= valuescalar{ field: 'age', operator: 'lte', value: 65 }

String operators

OperatorSQL equivalentValue typeExample
containsLIKE '%value%'string{ field: 'name', operator: 'contains', value: 'fleet' }
notContainsNOT LIKE '%value%'string{ field: 'name', operator: 'notContains', value: 'test' }
iContainsLOWER(col) LIKE LOWER('%value%')string{ field: 'email', operator: 'iContains', value: 'ACME' }
startsWithLIKE 'value%'string{ field: 'name', operator: 'startsWith', value: 'A' }
endsWithLIKE '%value'string{ field: 'email', operator: 'endsWith', value: '.com' }

Array operators

OperatorSQL equivalentValue typeExample
inIN (...)array{ field: 'status', operator: 'in', value: ['A', 'B'] }
notInNOT IN (...)array{ field: 'role', operator: 'notIn', value: ['banned'] }
isAnyOfIN (...)arrayAlias for in

isAnyOf is an alias for in. Both produce the same WHERE ... IN (...) clause. Use whichever reads better in your context.

Range operators

OperatorSQL equivalentValue typeExample
betweenBETWEEN low AND high[low, high]{ field: 'age', operator: 'between', value: [18, 65] }
notBetweenNOT BETWEEN low AND high[low, high]{ field: 'price', operator: 'notBetween', value: [0, 10] }

Unary operators (no value)

OperatorSQL equivalentExample
isNullIS NULL{ field: 'deletedAt', operator: 'isNull' }
isNotNullIS NOT NULL{ field: 'email', operator: 'isNotNull' }
isEmptyIS NULL OR = ''{ field: 'bio', operator: 'isEmpty' }
isNotEmptyIS NOT NULL AND != ''{ field: 'bio', operator: 'isNotEmpty' }
existsIS NOT NULL{ field: 'avatar', operator: 'exists' }
notExistsIS NULL{ field: 'avatar', operator: 'notExists' }

Operator aliases

For the common comparison operators you can also use their familiar SQL symbols. Aliases are normalized to the canonical operator before validation and query building, so { operator: '=' } behaves exactly like { operator: 'equals' }.

AliasCanonical
=, ==equals
!=, <>notEquals
>gt
>=gte
<lt
<=lte
{
  "where": [
    { "field": "status", "operator": "=", "value": "active" },
    { "field": "age", "operator": ">=", "value": 18 }
  ]
}

This makes migrating from a hand-rolled SQL-style query builder painless — the symbol operators your clients already send keep working. If you need the canonical form programmatically, the normalizeOperator() helper (and the OPERATOR_ALIASES map) are exported from the core package:

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

normalizeOperator('='); // 'equals'
normalizeOperator('<>'); // 'notEquals'
normalizeOperator('iContains'); // 'iContains' (canonical operators pass through)

Only scalar comparison operators have symbol aliases. Array, range, and unary operators (in, between, isNull, …) have no symbol form — use the canonical name.


Input formats

There are two ways to send operator-based filters: query string bracket notation (GET requests) and body format (POST requests).

Query string bracket notation

For GET requests, use bracket notation to specify operators:

GET /users?name[contains]=fleet&age[gte]=18&status=active

This is parsed into:

  • name with operator contains and value fleet
  • age with operator gte and value 18
  • status with implicit equals and value active

Bracket notation works with auto-fields. The adapter detects the operator object { contains: 'fleet' } and applies the correct SQL.

Body format with where array

For POST requests, send a where array of ColumnFilter objects in the request body:

{
  "where": [
    { "field": "name", "operator": "contains", "value": "fleet" },
    { "field": "age", "operator": "gte", "value": 18 },
    { "field": "status", "operator": "in", "value": ["active", "pending"] }
  ]
}

The FilterRunner extracts the where array, validates each ColumnFilter, and passes them to the adapter's applyColumnFilters() method. Any remaining keys in the body (outside where) are dispatched to @FilterFor methods as usual.

Controller setup for body format

@Post('search')
search(@ApplyFilter(UserFilter, { source: 'body' }) qb: QueryBuilder<User>) {
  return qb.getResultList();
}

AND/OR composition

Nested AND

All top-level filters in the where array are implicitly ANDed. You can also nest AND conditions:

{
  "where": [
    {
      "field": "status",
      "operator": "equals",
      "value": "active",
      "AND": [
        { "field": "age", "operator": "gte", "value": 18 },
        { "field": "age", "operator": "lte", "value": 65 }
      ]
    }
  ]
}

This produces: WHERE status = 'active' AND age >= 18 AND age <= 65.

Nested OR

{
  "where": [
    {
      "field": "role",
      "operator": "equals",
      "value": "admin",
      "OR": [
        { "field": "name", "operator": "contains", "value": "sync" },
        { "field": "email", "operator": "contains", "value": "sync" }
      ]
    }
  ]
}

This produces: WHERE role = 'admin' AND (name LIKE '%sync%' OR email LIKE '%sync%').


JSON sub-path filtering

When a field is a dotted path whose head segment is a JSON column (json/jsonb), the remaining segments are treated as keys inside the JSON value. No special operator or syntax is needed — you reuse the same dot-notation as relation paths, and the adapter detects that the head is a JSON column rather than a relation.

{
  "where": [
    { "field": "metadata.tier", "operator": "equals", "value": "pro" },
    { "field": "metadata.amount", "operator": "gte", "value": 100 }
  ]
}

Given a metadata JSON column, metadata.tier resolves to the tier key inside it, and metadata.amount to the amount key. Deeper paths work too — metadata.a.b extracts a.b. MikroORM compiles these to engine-specific JSON extraction (e.g. ->> on PostgreSQL).

Supported operators on a JSON sub-path:

GroupOperators
Comparisonequals, notEquals, gt, gte, lt, lte
Stringcontains
Arrayin
Rangebetween
UnaryisNull, isNotNull

JSON sub-paths can also be used for sorting — see Sort.

JSON sub-path filtering and sorting are implemented by the MikroORM adapter only. The TypeORM adapter resolves dotted paths as relations, not JSON keys.

Numeric WHERE comparisons on a JSON sub-path compare numerically on SQLite, MySQL, and PostgreSQL. But numeric sort by a JSON sub-path on PostgreSQL is lexical: ->> extracts text and ORDER BY does not auto-cast (unlike WHERE). Numeric JSON sort is correct on SQLite and MySQL. For portable ordering, sort by JSON sub-paths whose values are strings.


Mixed mode: operators + @FilterFor

You can use both approaches together. The FilterRunner processes where (column filters) first, then dispatches remaining keys to @FilterFor methods:

@Injectable()
@Filterable({ entity: User })
export class UserFilter extends MikroOrmFilter<User> {
  @FilterFor('search')
  applySearch(value: string) {
    // Custom full-text search logic
    this.$query.andWhere({
      $or: [
        { name: { $like: `%${escapeLike(value)}%` } },
        { email: { $like: `%${escapeLike(value)}%` } },
      ],
    });
  }
}

A POST body like this uses both:

{
  "where": [
    { "field": "status", "operator": "equals", "value": "active" }
  ],
  "search": "fleet"
}

The status filter is applied via applyColumnFilters, and search is dispatched to applySearch().


Validation with ColumnFilterDto

For class-validator integration, the core package exports ColumnFilterDto:

import { IsOptional, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { ColumnFilterDto } from '@dudousxd/nestjs-filter';

class SearchDto {
  @IsOptional()
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => ColumnFilterDto)
  where?: ColumnFilterDto[];
}

Runtime validation

The validateColumnFilter() and validateColumnFilters() functions are also available for manual validation:

import { validateColumnFilters, InvalidColumnFilterError } from '@dudousxd/nestjs-filter';

try {
  validateColumnFilters(filters);
} catch (err) {
  if (err instanceof InvalidColumnFilterError) {
    // Handle invalid filter
  }
}

The validator checks:

  • field is a non-empty string with only letters, digits, underscores, and dots
  • operator is one of the 22 known operators (or a symbol alias, which is normalized in place to its canonical form)
  • value matches the operator's expected type (scalar, array, tuple, or none for unary)
  • AND/OR arrays are validated recursively

Security

Field validation

The validateColumnFilter function rejects field names containing SQL-unsafe characters. Only [a-zA-Z_][a-zA-Z0-9_.]* is accepted, which prevents SQL injection through field names.

Parameterized queries

All values are parameterized by the ORM adapters. The TypeORM adapter uses named parameters (:paramName), and the MikroORM adapter uses FilterQuery objects. No raw SQL interpolation occurs.

LIKE escaping

All string operators (contains, notContains, iContains, startsWith, endsWith) escape LIKE wildcards (%, _, \) via the escapeLike() utility before interpolation.

Always validate and restrict which fields are queryable. Use the allowed option on @Filterable to whitelist accepted column names, or validate the field property in your own middleware before it reaches the filter.


ORM adapter support

Both the MikroORM and TypeORM adapters implement applyColumnFilters():

Translates ColumnFilter objects into MikroORM FilterQuery conditions using $like, $gte, $in, $and, $or, etc.

Translates ColumnFilter objects into TypeORM andWhere/orWhere calls with parameterized queries and Brackets for AND/OR groups.

On this page