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

smart-query-nestjs

v3.2.1

Published

High-performance search, filtering, pagination, and sorting for NestJS REST APIs

Readme

smart-query-nestjs

npm version NestJS v11 MIT License

A high-performance, ORM-agnostic NestJS library for search, filtering, pagination, and sorting in REST APIs.

Clean Code

  @Get()
  @UseInterceptors(new SmartQueryInterceptor(userQueryConfig))
  async findAll(@SmartQuery() query: SmartQueryResult<Prisma.UserWhereInput>) {
    const { where, orderBy, skip, take, page, limit } = query;

    const { data, total } = await this.userService.findAll({
      where: where,
      orderBy: orderBy,
      skip: skip,
      take: take,
      select: userSelectFields,
    });

    return {
      meta: {
        total,
        limit,
        page,
      },
      data,
    };
  }

Features

  • Global Search - Search across multiple fields with a single query parameter
  • Field Filtering - Filter by exact match, contains, startsWith, endsWith
  • Range Filtering - Greater than, less than, greater or equal, less or equal
  • Array Filtering - IN queries for multiple values
  • Nested Relation Filtering - Filter by related entity fields
  • Pagination - Page-based pagination with configurable limits
  • Multi-field Sorting - Sort by multiple fields with ascending/descending order
  • Prisma-optimized Queries - Returns Prisma-compatible orderBy arrays
  • Type-safe Query Results - Full TypeScript generics support
  • ORM Agnostic - Generates query objects compatible with any database layer (Prisma, TypeORM, etc.)
  • High Performance - Optimized parsing with single query parse and O(1) field lookups

Installation

npm install smart-query-nestjs

Requirements

  • NestJS v9, v10, or v11
  • TypeScript 5.0+

Quick Start

1. Configure the Module (Optional - Global Settings)

import { Module } from "@nestjs/common";
import { SmartQueryModule } from "smart-query-nestjs";

@Module({
  imports: [
    SmartQueryModule.forRoot({
      defaultLimit: 10,
      maxLimit: 100,
    }),
  ],
})
export class AppModule {}

Global configuration options:

  • defaultLimit - Default number of items per page (default: 10)
  • maxLimit - Maximum allowed items per page (default: 100)

2. Use in Controller

import { Controller, Get, UseInterceptors } from "@nestjs/common";
import {
  SmartQueryInterceptor,
  SmartQuery,
  buildSmartQuery,
  SmartQueryResult,
} from "smart-query-nestjs";
import { Prisma } from "@prisma/client";

@Controller("customers")
export class CustomerController {
  @Get()
  @UseInterceptors(
    new SmartQueryInterceptor({
      searchableFields: ["full_name", "email"],
      filterableFields: [
        "full_name",
        "email",
        "is_active",
        "status",
        "shop_id",
        "age",
      ],
      numberFields: ["age"],
      booleanFields: ["is_active"],
      dateFields: ["created_at"],
    }),
  )
  async findAll(
    @SmartQuery() query: SmartQueryResult<Prisma.CustomerWhereInput>,
  ) {
    const { where, orderBy, skip, take, page, limit } = query;

    const [data, total] = await Promise.all([
      this.prisma.customer.findMany({
        where,
        orderBy,
        skip,
        take,
      }),
      this.prisma.customer.count({ where }),
    ]);

    return { data, total };
  }
}

Auto-Response Transformation: If your endpoint returns { data, total }, the interceptor automatically enhances the response with pagination metadata:

{ "data": [...], "total": 100, "pagination": { "limit": 10, "total": 100, "totalPages": 10 } }

Query options (defined per-entity):

  • searchableFields - Fields to search when using searchTerm parameter
  • filterableFields - Fields that can be filtered
  • numberFields - Fields that should be parsed as numbers
  • booleanFields - Fields that should be parsed as booleans
  • dateFields - Fields that should be parsed as dates

Supported Query Formats

Global Search

Search across all searchable fields:

GET /customers?searchTerm=john

Field Filtering

Exact match filtering:

GET /customers?full_name=John
GET /customers?is_active=true

Range Filtering

Filter by numeric or date ranges:

GET /customers?price[gte]=10&price[lte]=100
GET /customers?created_at[gte]=2024-01-01
GET /customers?age[gt]=18

Operators: gte, gt, lte, lt

Array Filtering (IN Query)

Filter by multiple values:

GET /customers?status[]=pending&status[]=approved
GET /customers?status=pending,approved

Nested Relation Filtering

Filter by related entity fields:

GET /customers?shop.id=10
GET /customers?shop.name=MyShop

Pagination

GET /customers?page=2&limit=20
  • page: Page number (default: 1)
  • limit: Items per page (default: 10, max: 100)

Sorting

New Multi-field Syntax (Recommended)

GET /customers?sort=name,-createdAt
  • name → ascending
  • -createdAt → descending
  • Comma-separated values for multiple sort fields

Also works:

GET /customers?sort=firstName,createdAt

[{ firstName: 'asc' }, { createdAt: 'asc' }]

Generated Prisma query:

orderBy: [{ name: "asc" }, { createdAt: "desc" }];

Legacy Syntax (Backward Compatible)

Single field:

GET /customers?sortBy=created_at&sortOrder=desc

Multi-field:

GET /customers?sortBy=createdAt,firstName&sortOrder=desc,asc
  • sortBy: Comma-separated fields to sort by
  • sortOrder: Comma-separated order values (asc or desc), defaults to asc

Combined Example

GET /customers?searchTerm=john&status[]=active&age[gte]=18&page=1&limit=20&sort=name,-createdAt

This query will:

  • Search for "john" in all searchable fields
  • Filter by status "active"
  • Filter by age >= 18
  • Return page 1 with 20 items per page
  • Sort by name ascending, then by createdAt descending

TypeScript Support

SmartQueryResult Type

The package exports SmartQueryResult<TWhere, TOrderBy> for full TypeScript support:

import {
  SmartQuery,
  SmartQueryResult,
  SmartQueryInterceptor,
} from "smart-query-nestjs";
import { Prisma } from "@prisma/client";

@Controller("customers")
export class CustomerController {
  @Get()
  @UseInterceptors(
    new SmartQueryInterceptor({
      searchableFields: ["full_name", "email"],
      filterableFields: ["full_name", "email", "is_active", "status"],
    }),
  )
  async findAll(
    @SmartQuery()
    query: SmartQueryResult<
      Prisma.CustomerWhereInput,
      Prisma.CustomerOrderByWithRelationInput
    >,
  ) {
    const { where, orderBy, skip, take, page, limit } = query;
    // where: Prisma.CustomerWhereInput
    // orderBy: Prisma.CustomerOrderByWithRelationInput[]
    // skip: number
    // take: number
    // page: number (meta)
    // limit: number (meta)

    return this.prisma.customer.findMany({
      where,
      orderBy,
      skip,
      take,
    });
  }
}

SmartQueryPagination

interface SmartQueryPagination {
  skip: number;
  take: number;
}

SmartQueryResult

type SmartQueryResult<
  TWhere = any,
  TOrderBy = Record<string, "asc" | "desc">,
> = {
  where: TWhere;
  orderBy: TOrderBy[];
  skip: number;
  take: number;
  page: number;
  limit: number;
};

QueryConfig Type

The package exports a type-safe QueryConfig<T> utility for generating query configuration from Prisma models:

import { QueryConfig } from "smart-query-nestjs";
import { Prisma } from "@prisma/client";

type UserQueryConfig = QueryConfig<Prisma.UserWhereInput>;

const config: UserQueryConfig = {
  searchableFields: ["email", "name"],    // KeysOfType<T, string>
  filterableFields: ["id", "email", "role", "createdAt"],
  numberFields: ["age", "score"],         // KeysOfType<T, number>
  booleanFields: ["isActive"],             // KeysOfType<T, boolean>
  dateFields: ["createdAt", "updatedAt"],  // KeysOfType<T, Date>
};

KeysOfType Utility

type KeysOfType<T, U> = {
  [K in keyof T]: T[K] extends U | null | undefined ? K : never;
}[keyof T];

This utility extracts keys from a type where the value type extends U, including nullable fields (null | undefined). Works with Prisma models where fields can be optional or nullable.

API Reference

Interfaces

SmartQueryModuleOptions

Global configuration options for SmartQueryModule.forRoot():

interface SmartQueryModuleOptions {
  defaultLimit?: number;
  maxLimit?: number;
}

QueryOptions

Entity-specific query options for interceptor:

interface QueryOptions {
  searchableFields?: string[];
  filterableFields?: string[];
  numberFields?: string[];
  booleanFields?: string[];
  dateFields?: string[];
}

SmartQueryInterceptorOptions

Options for the SmartQueryInterceptor (extends QueryOptions):

interface SmartQueryInterceptorOptions extends QueryOptions {
  defaultLimit?: number;
  maxLimit?: number;
}

SmartQueryConfig

Full configuration (for backward compatibility):

interface SmartQueryConfig extends QueryOptions {
  defaultLimit?: number;
  maxLimit?: number;
}

PaginationOptions

interface PaginationOptions {
  page: number;
  limit: number;
  skip: number;
  sortBy: string;
  sortOrder: "asc" | "desc";
}

SmartQueryContext

Internal context object attached to the request:

type SmartQueryContext<TWhere = any> = {
  where: TWhere;
  orderBy: Record<string, "asc" | "desc">[];
  skip: number;
  take: number;
  page: number;
  limit: number;
};

BuiltSmartQuery

Return type of buildSmartQuery():

interface BuiltSmartQuery<TWhere = any> {
  where: TWhere;
  orderBy?: Record<string, "asc" | "desc">[];
  skip: number;
  take: number;
  page: number;
  limit: number;
}
Fully Type-Safe Usage
import { buildSmartQuery, SmartQueryResult, Prisma } from "smart-query-nestjs";

const query: SmartQueryResult<Prisma.UserWhereInput> = {
  where: { email: { contains: "@example.com" } },
  orderBy: [{ createdAt: "desc" }],
  skip: 0,
  take: 10,
  page: 1,
  limit: 10,
};

// Type-safe: result.where is Prisma.UserWhereInput
const result = buildSmartQuery(query);

// Add extra conditions while preserving types
const filtered = buildSmartQuery(query, { shop_id: 1 });
// filtered.where.shop_id is typed as number

Decorators

@SmartQuery()

Extracts the SmartQueryResult from the request.

@Get()
async findAll(@SmartQuery() query: SmartQueryResult) {
  const { where, orderBy, skip, take, page, limit } = query;
  // ...
}

Classes

SmartQueryInterceptor

NestJS interceptor for parsing query parameters.

@UseInterceptors(new SmartQueryInterceptor({
  searchableFields: ['name', 'email'],
  filterableFields: ['name', 'email', 'status'],
}))

Functions

buildSmartQuery(context, ...extraConditions)

Merges the smart query context with additional conditions and generates a database query object. Fully generic with full TypeScript support.

import { buildSmartQuery, SmartQueryResult, Prisma } from "smart-query-nestjs";

const query: SmartQueryResult<Prisma.UserWhereInput> = {
  where: { status: "active" },
  orderBy: [{ name: "asc" }],
  skip: 0,
  take: 10,
  page: 1,
  limit: 10,
};

// Basic usage - result.where is Prisma.UserWhereInput
const result = buildSmartQuery(query);

// With extra conditions - merges with AND
const withExtra = buildSmartQuery(query, { shop_id: 1 });
// withExtra.where is { AND: [query.where, { shop_id: 1 }] }

// Multiple extra conditions
const withMultiple = buildSmartQuery(query, { shop_id: 1 }, { is_deleted: false });

Function Signature:

function buildSmartQuery<TWhere = any>(
  query: SmartQueryResult<TWhere>,
  ...extraConditions: Partial<TWhere>[]
): BuiltSmartQuery<TWhere>

createSmartQueryInterceptor(config)

Factory function to create a SmartQueryInterceptor with specific options.

const interceptor = createSmartQueryInterceptor({
  searchableFields: ["title", "description"],
  filterableFields: ["category", "price", "status"],
  numberFields: ["price"],
});

Utility Functions

parseQueryString(queryString)

Parses a query string into an object.

const parsed = parseQueryString("page=1&limit=10&searchTerm=foo");
// Returns: { page: 1, limit: 10, searchTerm: 'foo' }

pick(obj, keys)

Pick specific keys from an object.

const picked = pick(user, ["id", "name", "email"]);
// Returns: { id: ..., name: ..., email: ... }

Parsers

parseFilters(parsedQuery, config)

Parses filter parameters from the query string.

buildSearchConditions(searchTerm, config)

Builds search conditions for the searchTerm parameter.

parsePagination(parsedQuery, config)

Parses pagination parameters (page, limit).

parseSort(sort, sortBy, sortOrder)

Parses sorting parameters.

Performance Optimizations

The library includes several performance optimizations:

  1. Single Query Parse - Uses qs.parse with allowDots: true to parse the query string only once
  2. O(1) Field Lookups - Uses Set for field lookups instead of array includes
  3. Modular Architecture - Separates concerns into dedicated parsers
  4. No Unnecessary Cloning - Avoids deep object cloning where possible

Different Entities, Different Options

Each controller/entity can have its own configuration:

// For Customers
@UseInterceptors(new SmartQueryInterceptor({
  searchableFields: ['full_name', 'email'],
  filterableFields: ['full_name', 'email', 'status', 'shop_id'],
}))

// For Products
@UseInterceptors(new SmartQueryInterceptor({
  searchableFields: ['name', 'description', 'sku'],
  filterableFields: ['name', 'category', 'price', 'is_active'],
  numberFields: ['price', 'stock'],
}))

// For Orders
@UseInterceptors(new SmartQueryInterceptor({
  searchableFields: ['order_number'],
  filterableFields: ['status', 'customer_id', 'total'],
  numberFields: ['total'],
  dateFields: ['created_at'],
}))

Architecture

This library follows a two-level configuration architecture:

  1. Global Configuration (SmartQueryModule.forRoot()) - System-level settings that apply globally
  2. Query Options - Entity-specific settings defined at the interceptor level

Why This Architecture?

Searchable and filterable fields are model-specific. Different entities (User, Product, Order, etc.) require different fields. Defining these globally was poor architecture because:

  • You'd need to define all possible fields for all entities in one place
  • Adding a new entity required updating the global config
  • It's not clear which fields belong to which entity

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Author

Nurul Islam Rimon