npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@elchinabilov/nestjs-restapi-filters

v1.1.2

Published

Strapi-style REST API filter engine for NestJS. Filter any in-memory array with query-parameter syntax like filters[field][$operator]=value

Readme

@elchinabilov/nestjs-restapi-filters

Strapi-style REST API filter engine for NestJS. Filter any in-memory array with query-parameter syntax.

GET /api/users?filters[name][$eq]=John&filters[age][$gte]=18

Installation

npm install @elchinabilov/nestjs-restapi-filters

Quick Start

1. Import the module

import { Module } from '@nestjs/common';
import { RestApiFiltersModule } from '@elchinabilov/nestjs-restapi-filters';

@Module({
  imports: [RestApiFiltersModule],
})
export class AppModule {}

2. Use in a controller

import { Controller, Get } from '@nestjs/common';
import {
  Filters,
  FilterQuery,
  FilterEngineService,
} from '@elchinabilov/nestjs-restapi-filters';

@Controller('users')
export class UsersController {
  private users = [
    { id: 1, name: 'John',  age: 25, role: 'admin' },
    { id: 2, name: 'Jane',  age: 30, role: 'user' },
    { id: 3, name: 'Bob',   age: 22, role: 'user' },
    { id: 4, name: 'Alice', age: 35, role: 'admin' },
  ];

  constructor(private readonly filterEngine: FilterEngineService) {}

  @Get()
  findAll(@Filters() filters: FilterQuery) {
    return this.filterEngine.applyFilters(this.users, filters);
  }
}

Now you can query:

GET /users?filters[role][$eq]=admin
GET /users?filters[age][$gte]=25&filters[role][$eq]=user
GET /users?filters[name][$containsi]=jo

Standalone Usage (without DI)

import { applyFilters } from '@elchinabilov/nestjs-restapi-filters';

const data = [
  { id: 1, name: 'John', age: 25 },
  { id: 2, name: 'Jane', age: 30 },
];

const result = applyFilters(data, {
  age: { $gte: 25 },
  name: { $startsWith: 'J' },
});
// → [{ id: 1, name: 'John', age: 25 }, { id: 2, name: 'Jane', age: 30 }]

Available Operators

| Operator | Description | Example | | --------------- | ----------------------------------- | ----------------------------------------------------------- | | $eq | Equal | filters[name][$eq]=John | | $eqi | Equal (case-insensitive) | filters[name][$eqi]=john | | $ne | Not equal | filters[role][$ne]=admin | | $nei | Not equal (case-insensitive) | filters[role][$nei]=ADMIN | | $lt | Less than | filters[age][$lt]=30 | | $lte | Less than or equal to | filters[age][$lte]=30 | | $gt | Greater than | filters[age][$gt]=18 | | $gte | Greater than or equal to | filters[age][$gte]=18 | | $in | Included in an array | filters[id][$in][0]=1&filters[id][$in][1]=2 | | $notIn | Not included in an array | filters[id][$notIn][0]=3&filters[id][$notIn][1]=4 | | $contains | Contains substring | filters[name][$contains]=ohn | | $notContains | Does not contain substring | filters[name][$notContains]=xyz | | $containsi | Contains (case-insensitive) | filters[name][$containsi]=OHN | | $notContainsi | Does not contain (case-insensitive) | filters[name][$notContainsi]=XYZ | | $startsWith | Starts with | filters[name][$startsWith]=Jo | | $startsWithi | Starts with (case-insensitive) | filters[name][$startsWithi]=jo | | $endsWith | Ends with | filters[name][$endsWith]=hn | | $endsWithi | Ends with (case-insensitive) | filters[name][$endsWithi]=HN | | $null | Is null | filters[avatar][$null]=true | | $notNull | Is not null | filters[avatar][$notNull]=true | | $between | Between two values | filters[age][$between][0]=18&filters[age][$between][1]=30 |

Logical Operators

| Operator | Description | Example | | -------- | ------------------------- | ------------------------------------------------------------------ | | $and | All conditions must match | filters[$and][0][age][$gte]=18&filters[$and][1][role][$eq]=admin | | $or | At least one must match | filters[$or][0][role][$eq]=admin&filters[$or][1][role][$eq]=user | | $not | Negate a condition | filters[$not][role][$eq]=admin |

Note: Multiple fields at the same level are implicitly combined with $and.


Deep Filtering (Nested Fields)

Filter on nested object properties:

GET /api/books?filters[author][name][$eq]=John
const books = [
  { id: 1, title: 'Book A', author: { name: 'John', country: 'US' } },
  { id: 2, title: 'Book B', author: { name: 'Jane', country: 'UK' } },
];

const result = applyFilters(books, {
  author: { name: { $eq: 'John' } },
});
// → [{ id: 1, title: 'Book A', author: { name: 'John', country: 'US' } }]

Complex Filtering

Combine $and, $or, and $not for advanced queries:

GET /api/books?filters[$and][0][$or][0][date][$eq]=2020-01-01&filters[$and][0][$or][1][date][$eq]=2020-01-02&filters[$and][1][author][name][$eq]=John
const result = applyFilters(books, {
  $and: [
    {
      $or: [
        { date: { $eq: '2020-01-01' } },
        { date: { $eq: '2020-01-02' } },
      ],
    },
    {
      author: { name: { $eq: 'John' } },
    },
  ],
});

Validation Pipe

Use ParseFiltersPipe to validate and sanitize incoming filter queries:

import { Filters, ParseFiltersPipe, FilterQuery } from '@elchinabilov/nestjs-restapi-filters';

@Get()
findAll(
  @Filters(new ParseFiltersPipe({ maxDepth: 5, strict: true }))
  filters: FilterQuery,
) {
  return this.filterEngine.applyFilters(this.data, filters);
}

| Option | Default | Description | | ---------- | ------- | -------------------------------------- | | maxDepth | 10 | Maximum nesting depth (DoS protection) | | strict | false | Throw on unknown operators like $foo |


Module Configuration

RestApiFiltersModule.forRoot({
  autoCoerce: true,   // auto-convert '18' → 18, 'true' → true  (default: true)
  maxDepth: 5,        // max filter nesting depth                (default: 10)
});

Auto Type Coercion

Query parameters are always strings. By default, the engine auto-coerces:

| Input string | Coerced value | | ------------ | ------------- | | 'true' | true | | 'false' | false | | 'null' | null | | '42' | 42 | | '3.14' | 3.14 | | 'hello' | 'hello' |

Disable with { autoCoerce: false }.


TypeORM Integration (Database-level Filtering)

Filter directly at the database level by chaining .applyFilters() on any TypeORM SelectQueryBuilder.

Setup (2 steps)

Step 1. Add the type declaration to your project.

Create a file src/types/typeorm-filters.d.ts (or any .d.ts inside your src/):

import { FilterQuery, TypeOrmFilterOptions } from '@elchinabilov/nestjs-restapi-filters';

declare module 'typeorm' {
  interface SelectQueryBuilder<Entity> {
    applyFilters(filters: FilterQuery, options?: TypeOrmFilterOptions): this;
  }
}

Step 2. Call extendQueryBuilderWithFilters() once at application startup:

// main.ts
import { extendQueryBuilderWithFilters } from '@elchinabilov/nestjs-restapi-filters';

extendQueryBuilderWithFilters();

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Step 1 provides TypeScript type support (autocomplete, type checking). Step 2 adds the actual runtime method to the QueryBuilder prototype.

Usage in a service / repository

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Filters, FilterQuery } from '@elchinabilov/nestjs-restapi-filters';
import { Order } from './order.entity';

@Injectable()
export class OrderService {
  constructor(
    @InjectRepository(Order)
    private readonly orderRepo: Repository<Order>,
  ) {}

  async findAll(filters: FilterQuery) {
    const qb = this.orderRepo.createQueryBuilder('order');

    const [data, total] = await qb
      .leftJoin('order.user', 'user')
      .addSelect(['user.name', 'user.surname', 'user.phone'])
      .leftJoin('order.organization', 'organization')
      .addSelect([
        'organization.id',
        'organization.name',
        'organization.slug',
      ])
      .applyFilters(filters)
      .orderBy('order.createdAt', 'DESC')
      .getManyAndCount();

    return { data, total };
  }
}

Controller

@Controller('orders')
export class OrderController {
  constructor(private readonly orderService: OrderService) {}

  @Get()
  findAll(@Filters() filters: FilterQuery) {
    return this.orderService.findAll(filters);
  }
}

How alias resolution works

The method auto-detects all joined aliases from the query builder and resolves fields in 3 ways:

| Scenario | Filter | Generated SQL (MySQL) | | -------- | ------ | --------------------- | | Regular column | filters[status][$eq]=active | order.status = :_filter_0 | | Joined relation | filters[user][name][$containsi]=john | LOWER(user.name) LIKE :_filter_0 | | JSON column | filters[meta][pricing][total][$gte]=100 | JSON_UNQUOTE(JSON_EXTRACT(order.meta, '$.pricing.total')) >= :_filter_0 | | JSON column (shallow) | filters[serviceMethod][name][$containsi]=test | LOWER(JSON_UNQUOTE(JSON_EXTRACT(order.serviceMethod, '$.name'))) LIKE :_filter_0 | | Logical | filters[$or][0][status][$eq]=pending&... | (order.status = :_filter_0 OR order.status = :_filter_1) |

Resolution logic:

  1. Field matches a joined alias → relation column (e.g. user.name)
  2. Field has direct operators ($eq, $gte, ...) → regular column (e.g. order.status)
  3. Field not an alias, nested objectsJSON column with JSON_EXTRACT (MySQL), #>> (PostgreSQL), json_extract (SQLite), JSON_VALUE (MSSQL)

JSON Column Filtering

Nested filters on non-joined fields are automatically treated as JSON column paths:

GET /orders?filters[meta][pricing][total][$gte]=100
GET /orders?filters[serviceMethod][name][$containsi]=test
GET /orders?filters[deliveryAddress][city][$eq]=Baku

Database type is auto-detected from the TypeORM connection. You can also set it manually:

qb.applyFilters(filters, { dbType: 'postgres' });

| Database | JSON extraction syntax | | ---------------- | --------------------------------------------------------- | | MySQL / MariaDB | JSON_UNQUOTE(JSON_EXTRACT(col, '$.path')) | | PostgreSQL | col #>> '{path,to,field}' | | SQLite | json_extract(col, '$.path') | | MSSQL | JSON_VALUE(col, '$.path') |

Options

qb.applyFilters(filters, {
  alias: 'o',           // override root entity alias (default: qb.alias)
  autoCoerce: false,    // disable auto type coercion
  dbType: 'postgres',   // override database type (default: auto-detected)
});

API Reference

FilterEngineService

| Method | Signature | Description | | -------------- | -------------------------------------------------------------------------------------- | ---------------------------------- | | applyFilters | applyFilters<T>(data: T[], filters: FilterQuery, options?: FilterEngineOptions): T[] | Filter an array by the given query |

applyFilters() (standalone)

import { applyFilters } from '@elchinabilov/nestjs-restapi-filters';
const result = applyFilters(data, filters, options?);

@Filters() (decorator)

@Get()
findAll(@Filters() filters: FilterQuery) { ... }

// Custom query key:
@Get()
findAll(@Filters('filter') filters: FilterQuery) { ... }

.applyFilters() (TypeORM QueryBuilder)

import { extendQueryBuilderWithFilters } from '@elchinabilov/nestjs-restapi-filters';

// Call once at startup
extendQueryBuilderWithFilters();

// Then use on any SelectQueryBuilder
const [data, total] = await qb
  .applyFilters(filters)
  .getManyAndCount();

License

MIT