nestjs-filter Integration
Type-safe filter queries with operators, sort, pagination, and search when using @ApplyFilter from nestjs-filter.
const query = api.users.list.filterQuery();
query.contains('name', 'Al');
query.in('status', ['active', 'pending']);
query.sort('name', 'asc');
query.page(0, 25);
const { data } = useQuery(api.users.list.queryOptions(query.build()));When a controller uses @ApplyFilter(FilterClass) from @dudousxd/nestjs-filter, the codegen detects it and generates typed filter queries automatically. No extra configuration needed.
How it works
The codegen reads the filter class and generates TypedFilterQuery<'field1' | 'field2'> as the query type. When autoFields: true is set, fields are resolved directly from the entity — including dot-notation paths for relations. Routes with filters also get a filterQuery() helper.
// src/pipeline-runs.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
import { ApplyFilter } from '@dudousxd/nestjs-filter';
import { PipelineRunFilter } from './pipeline-run.filter';
@Controller('/api/pipeline-runs')
export class PipelineRunsController {
@Get()
list(@ApplyFilter(PipelineRunFilter) qb: QueryBuilder) {
return qb.getResultList();
}
@Post('search')
search(@ApplyFilter(PipelineRunFilter, { source: 'body' }) qb: QueryBuilder) {
return qb.getResultList();
}
}autoFields and relation traversal
With autoFields: true, the filter class needs no explicit properties. The codegen resolves fields from the entity and recursively traverses relations (OneToMany, ManyToOne, etc.) to generate dot-notation fields like tasks.status and tasks.name. Keys from @Relations decorators are included too.
// src/pipeline-run.filter.ts
import { Filterable, FilterFor, MikroOrmFilter } from '@dudousxd/nestjs-filter';
import { PipelineRun } from './pipeline-run.entity';
@Filterable({ entity: PipelineRun, autoFields: true })
export class PipelineRunFilter extends MikroOrmFilter<PipelineRun> {
@FilterFor("search")
applySearch(value: string) {
this.whereLike("name", value);
}
}No name?: string or status?: string declarations. The codegen reads the PipelineRun entity and pulls every scalar field plus relation paths.
The codegen generates:
// .nestjs-inertia/api.ts (generated)
pipelineRuns: {
list: {
method: "GET";
query: import('@dudousxd/nestjs-filter-client').TypedFilterQuery<
"name" | "status" | "createdAt" | "tasks.status" | "tasks.name"
>;
// ...
},
search: {
method: "POST";
body: import('@dudousxd/nestjs-filter-client').FilterQueryResult<
"name" | "status" | "createdAt" | "tasks.status" | "tasks.name"
>;
// ...
}
}You can still declare explicit properties on the filter class. When autoFields is off (default), the codegen uses those properties as the field union. When autoFields: true, explicit properties are ignored in favor of entity introspection.
GET endpoints — queryOptions with filters
Pass filter, sort, pagination, and search as a typed object:
import { useQuery } from '@tanstack/react-query';
import { api } from '~codegen/api';
const { data } = useQuery(
api.pipelineRuns.list.queryOptions({
filter: { name: 'nightly', status: 'RUNNING' },
sort: [{ field: 'createdAt', direction: 'desc' }],
paginate: { page: 0, size: 25 },
search: 'nightly',
})
);| Field | Type | Description |
|---|---|---|
filter | { [field]?: value } | Filter conditions (field names restricted to entity fields) |
sort | Array<{ field, direction }> | Sort directives (field names restricted) |
paginate | { page, size } | Offset-based pagination |
search | string | Global search term |
include | string[] | Relation paths to eager-load |
GET endpoints — filterQuery() builder
For queries with operators (contains, gte, in, between, etc.), use the generated filterQuery() helper:
import { useQuery } from '@tanstack/react-query';
import { api } from '~codegen/api';
const query = api.pipelineRuns.list.filterQuery();
query.contains('name', 'nightly');
query.in('status', ['RUNNING', 'COMPLETED']);
query.sort('createdAt', 'desc');
query.page(0, 25);
const { data } = useQuery(api.pipelineRuns.list.queryOptions(query.build()));The builder is fully typed. Only fields from the entity (or the filter class, when autoFields is off) are accepted:
api.pipelineRuns.list.filterQuery()
.in('status', ['RUNNING']) // ✓ 'status' exists on PipelineRun
.contains('tasks.name', 'deploy') // ✓ dot-notation relation field
.contains('foo', 'bar'); // ✗ TypeScript error: 'foo' is not a valid fieldField union as a type
Need the filterable-field union on its own — e.g. to validate dynamic column ids from a data-grid before passing them to the builder? Use the Route.FilterFields<K> helper instead of reaching into the builder with Parameters<...>:
import type { Route } from '~codegen/api';
type RunFilterField = Route.FilterFields<'pipelineRuns.list'>;
// → 'status' | 'tasks.name' | ...
function isRunField(id: string): id is RunFilterField {
return runFields.includes(id as RunFilterField);
}For a route without a filter, Route.FilterFields<K> resolves to never.
Available operators
| Operator | Example |
|---|---|
equals | .equals('status', 'RUNNING') |
contains | .contains('name', 'nightly') |
startsWith | .startsWith('name', 'pipe') |
endsWith | .endsWith('name', 'run') |
gt / gte / lt / lte | .gte('createdAt', '2025-01-01') |
in / notIn | .in('status', ['RUNNING', 'COMPLETED']) |
between | .between('duration', 0, 3600) |
isNull / isNotNull | .isNull('deletedAt') |
isEmpty / isNotEmpty | .isEmpty('notes') |
Sort and pagination
const query = api.pipelineRuns.list.filterQuery();
query.contains('name', 'nightly');
query.sort('createdAt', 'desc');
query.page(0, 25);
const { data } = useQuery(api.pipelineRuns.list.queryOptions(query.build()));POST search endpoints — source: "body"
When @ApplyFilter(Filter, { source: "body" }) is used, the codegen places the filter type on body (as FilterQueryResult) instead of query. This is for POST search endpoints where the filter payload goes in the request body.
The codegen generates both mutationOptions() and queryOptions(body) for POST filter routes. This lets you use useQuery for search endpoints that happen to use POST:
import { useQuery } from '@tanstack/react-query';
import { api } from '~codegen/api';
// Build the filter
const query = api.searchPipelineRuns.search.filterQuery();
query.in('status', ['RUNNING', 'COMPLETED']);
query.sort('createdAt', 'desc');
query.page(0, 25);
// useQuery — not useMutation — for a POST search
const { data } = useQuery(
api.searchPipelineRuns.search.queryOptions(query.build()),
);You can also pass an inline object instead of using the builder:
const { data } = useQuery(
api.searchPipelineRuns.search.queryOptions({
filter: { status: 'RUNNING' },
sort: [{ field: 'createdAt', direction: 'desc' }],
paginate: { page: 0, size: 25 },
}),
);mutationOptions() is still generated for POST filter routes when you need useMutation semantics (optimistic updates, manual triggering, etc.). queryOptions(body) is the addition — it wraps the POST call in a query key so React Query can cache and refetch it like a GET.
Install
The codegen detects @ApplyFilter automatically. You only need @dudousxd/nestjs-filter-client installed for the TypedFilterQuery type and filterQueryTyped runtime:
pnpm add @dudousxd/nestjs-filter-clientnpm install @dudousxd/nestjs-filter-clientyarn add @dudousxd/nestjs-filter-clientIf @dudousxd/nestjs-filter-client is not installed, the codegen falls back to query: never for @ApplyFilter routes. No error, just no filter types.
Using with setGlobalPrefix
How to serve Inertia pages alongside an existing API that uses NestJS setGlobalPrefix — the middleware gap and how to fix it.
Not Found Filter
A production-ready NestJS exception filter that renders an Inertia component on 404 for page requests and returns structured JSON for API routes.