Skip to main content
The @nestjsx/crud-request package provides utilities for building and parsing complex queries. It supports filtering, sorting, pagination, field selection, and relation joins through URL query parameters.

Query parameters

Clients can control API responses using standardized query parameters:

Field selection

Select specific fields to include in the response:
GET /companies?fields=name,domain
GET /companies?select=id,name,description
Both fields and select parameters work identically.

Filtering

Filter results using comparison operators:
# Exact match
GET /companies?filter=name||$eq||Acme Corp

# Contains (case-sensitive)
GET /companies?filter=name||$cont||Tech

# Greater than
GET /companies?filter=id||$gt||100

# Multiple filters (AND logic)
GET /companies?filter=name||$cont||Tech&filter=id||$gt||50

Comparison operators

The framework supports these filter operators:
OperatorDescriptionExample
$eqEquals`?filter=id$eq1`
$neNot equals`?filter=status$nedraft`
$gtGreater than`?filter=age$gt18`
$ltLess than`?filter=price$lt100`
$gteGreater than or equal`?filter=score$gte50`
$lteLess than or equal`?filter=stock$lte10`
$startsStarts with`?filter=name$startsA`
$endsEnds with`?filter=email$ends@example.com`
$contContains`?filter=description$contkeyword`
$exclExcludes`?filter=name$excltest`
$inIn array`?filter=status$inactive,pending`
$notinNot in array`?filter=role$notinadmin,moderator`
$isnullIs NULL`?filter=deletedAt$isnull`
$notnullIs not NULL`?filter=publishedAt$notnull`
$betweenBetween values`?filter=age$between18,65`
Case-insensitive variants are available by appending L to the operator: $eqL, $contL, $startsL, etc.
From /home/daytona/workspace/source/packages/crud-typeorm/src/typeorm-crud.service.ts:854-971:

OR conditions

Use the or parameter for OR logic:
# Name contains "Tech" OR name contains "Software"
GET /companies?or=name||$cont||Tech&or=name||$cont||Software
For complex queries, use the s (search) parameter with JSON:
GET /companies?s={"$and":[{"name":{"$cont":"Tech"}},{"id":{"$gt":10}}]}
Search supports nested $and and $or conditions:
{
  "$and": [
    { "name": { "$cont": "Tech" } },
    {
      "$or": [
        { "domain": { "$ends": ".com" } },
        { "domain": { "$ends": ".io" } }
      ]
    }
  ]
}
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:103-108:

Sorting

Sort results by one or more fields:
# Sort by name ascending
GET /companies?sort=name,ASC

# Sort by multiple fields
GET /companies?sort=name,ASC&sort=createdAt,DESC

Pagination

Paginate results using limit/offset or page-based pagination:
# Limit and offset
GET /companies?limit=10&offset=20

# Page-based (requires limit)
GET /companies?page=3&limit=10
When pagination is enabled, responses include metadata:
{
  "data": [...],
  "count": 10,
  "total": 250,
  "page": 3,
  "pageCount": 25
}

Joining relations

Load related entities:
# Join a single relation
GET /companies?join=users

# Join and select specific fields
GET /companies?join=users||name,email

# Join multiple relations
GET /companies?join=users&join=projects

# Join nested relations
GET /companies?join=users.profile
Relations must be configured in the controller’s query.join options before they can be requested.

Cache control

Control query caching:
# Bypass cache
GET /companies?cache=0

Soft deletes

Include soft-deleted records:
GET /companies?include_deleted=1

Request query builder

The RequestQueryBuilder class helps build query strings programmatically in frontend applications:
import { RequestQueryBuilder } from '@nestjsx/crud-request';

const queryString = RequestQueryBuilder.create()
  .select(['name', 'domain'])
  .search({
    $and: [
      { name: { $cont: 'Tech' } },
      { id: { $gt: 10 } }
    ]
  })
  .sortBy({ field: 'name', order: 'ASC' })
  .setLimit(10)
  .setPage(1)
  .setJoin({ field: 'users', select: ['name', 'email'] })
  .query();

// Result: fields=name,domain&s={...}&sort=name,ASC&limit=10&page=1&join=users||name,email
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:24-243:

Builder methods

select()

Select specific fields:
RequestQueryBuilder.create()
  .select(['name', 'email', 'createdAt'])
  .query();
// fields=name,email,createdAt
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:95-100: Add complex search conditions:
RequestQueryBuilder.create()
  .search({
    $or: [
      { status: 'active' },
      { status: 'pending' }
    ]
  })
  .query();
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:103-108:

setFilter() and setOr()

Add filter conditions:
// AND filters
RequestQueryBuilder.create()
  .setFilter({ field: 'name', operator: '$cont', value: 'Tech' })
  .setFilter({ field: 'id', operator: '$gt', value: 10 })
  .query();
// filter=name||$cont||Tech&filter=id||$gt||10

// OR filters
RequestQueryBuilder.create()
  .setOr({ field: 'status', operator: '$eq', value: 'active' })
  .setOr({ field: 'status', operator: '$eq', value: 'pending' })
  .query();
// or=status||$eq||active&or=status||$eq||pending
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:110-118: Filters can also be passed as arrays:
RequestQueryBuilder.create()
  .setFilter(['name', '$cont', 'Tech'])
  .query();

setJoin()

Join relations:
RequestQueryBuilder.create()
  .setJoin({ field: 'users' })
  .setJoin({ field: 'projects', select: ['name', 'status'] })
  .query();
// join=users&join=projects||name,status
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:120-131:

sortBy()

Sort results:
RequestQueryBuilder.create()
  .sortBy({ field: 'createdAt', order: 'DESC' })
  .sortBy({ field: 'name', order: 'ASC' })
  .query();
// sort=createdAt,DESC&sort=name,ASC
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:133-144:

setLimit(), setOffset(), setPage()

Pagination controls:
// Limit and offset
RequestQueryBuilder.create()
  .setLimit(25)
  .setOffset(50)
  .query();
// limit=25&offset=50

// Page-based
RequestQueryBuilder.create()
  .setLimit(25)
  .setPage(3)
  .query();
// limit=25&page=3
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:146-169:

resetCache()

Bypass cache:
RequestQueryBuilder.create()
  .resetCache()
  .query();
// cache=0
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:161-164:

setIncludeDeleted()

Include soft-deleted records:
RequestQueryBuilder.create()
  .setIncludeDeleted(1)
  .query();
// include_deleted=1
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:166-169:

Create from params

Build a query from a params object:
import { RequestQueryBuilder } from '@nestjsx/crud-request';

const query = RequestQueryBuilder.create({
  fields: ['name', 'email'],
  search: {
    $and: [
      { status: 'active' },
      { role: { $in: ['admin', 'user'] } }
    ]
  },
  filter: [
    { field: 'createdAt', operator: '$gte', value: '2024-01-01' }
  ],
  join: [
    { field: 'company' },
    { field: 'profile', select: ['avatar', 'bio'] }
  ],
  sort: [
    { field: 'createdAt', order: 'DESC' }
  ],
  limit: 20,
  page: 1,
}).query();
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:196-211:

Custom query options

Customize parameter names globally:
import { RequestQueryBuilder } from '@nestjsx/crud-request';

RequestQueryBuilder.setOptions({
  delim: '||',
  delimStr: ',',
  paramNamesMap: {
    fields: ['fields', 'select'],
    search: 's',
    filter: 'filter',
    or: 'or',
    join: 'join',
    sort: 'sort',
    limit: ['limit', 'per_page'],
    offset: 'offset',
    page: 'page',
    cache: 'cache',
    includeDeleted: 'include_deleted',
  },
});
From /home/daytona/workspace/source/packages/crud-request/src/request-query.builder.ts:25-41:

ParsedRequest decorator

In controllers, access parsed request data using the @ParsedRequest() decorator:
import {
  Controller,
  Get,
} from '@nestjs/common';
import {
  Crud,
  CrudRequest,
  ParsedRequest,
  Override,
} from '@nestjsx/crud';
import { Company } from './company.entity';
import { CompaniesService } from './companies.service';

@Crud({
  model: { type: Company },
})
@Controller('companies')
export class CompaniesController {
  constructor(public service: CompaniesService) {}

  @Override('getManyBase')
  @Get()
  async getMany(@ParsedRequest() req: CrudRequest) {
    // Access parsed query parameters
    console.log(req.parsed.fields);
    console.log(req.parsed.filter);
    console.log(req.parsed.join);
    console.log(req.parsed.sort);
    console.log(req.parsed.limit);
    console.log(req.parsed.offset);
    
    return this.service.getMany(req);
  }
}
The CrudRequest interface contains:
interface CrudRequest {
  parsed: ParsedRequestParams;  // Parsed query parameters
  options: CrudRequestOptions;  // Controller configuration
}

interface ParsedRequestParams {
  fields: string[];              // Selected fields
  paramsFilter: QueryFilter[];   // Path parameter filters
  search: SCondition;            // Search conditions
  filter: QueryFilter[];         // Filter conditions
  or: QueryFilter[];             // OR filter conditions
  join: QueryJoin[];             // Join relations
  sort: QuerySort[];             // Sort orders
  limit: number;                 // Limit value
  offset: number;                // Offset value
  page: number;                  // Page number
  cache: number;                 // Cache setting
  includeDeleted: number;        // Include deleted flag
}

Validation

Query parameters are automatically validated. Invalid requests return 400 Bad Request:
# Invalid operator
GET /companies?filter=name||$invalid||value
# 400: Invalid operator

# Invalid field (not in 'allow' list)
GET /companies?filter=secretField||$eq||value
# 400: Invalid field

# Invalid join (not configured)
GET /companies?join=invalidRelation
# 400: Invalid relation
Validation is based on the controller’s query configuration. Fields, filters, and joins must be explicitly allowed.

Examples

Frontend integration

React example using the query builder:
import { RequestQueryBuilder } from '@nestjsx/crud-request';
import axios from 'axios';

interface Company {
  id: number;
  name: string;
  domain: string;
}

interface GetManyResponse {
  data: Company[];
  count: number;
  total: number;
  page: number;
  pageCount: number;
}

async function fetchCompanies(
  searchTerm: string,
  page: number = 1
): Promise<GetManyResponse> {
  const query = RequestQueryBuilder.create()
    .search({
      $or: [
        { name: { $cont: searchTerm } },
        { domain: { $cont: searchTerm } }
      ]
    })
    .sortBy({ field: 'name', order: 'ASC' })
    .setLimit(20)
    .setPage(page)
    .setJoin({ field: 'users' })
    .query();

  const response = await axios.get<GetManyResponse>(
    `/api/companies?${query}`
  );
  
  return response.data;
}

Complex filtering

Combine multiple filter types:
const query = RequestQueryBuilder.create({
  search: {
    $and: [
      {
        $or: [
          { status: 'active' },
          { status: 'pending' }
        ]
      },
      { createdAt: { $gte: '2024-01-01' } },
      { deletedAt: { $isnull: true } }
    ]
  },
  join: [
    { field: 'company', select: ['name'] },
    { field: 'profile' }
  ],
  sort: [
    { field: 'priority', order: 'DESC' },
    { field: 'createdAt', order: 'DESC' }
  ],
  limit: 50,
  page: 1,
}).query();

Next steps

Controllers

Learn how to configure query options in controllers

Services

Understand how services process requests