smart-query-nestjs
v3.2.1
Published
High-performance search, filtering, pagination, and sorting for NestJS REST APIs
Maintainers
Readme
smart-query-nestjs
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-nestjsRequirements
- 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 usingsearchTermparameterfilterableFields- Fields that can be filterednumberFields- Fields that should be parsed as numbersbooleanFields- Fields that should be parsed as booleansdateFields- Fields that should be parsed as dates
Supported Query Formats
Global Search
Search across all searchable fields:
GET /customers?searchTerm=johnField Filtering
Exact match filtering:
GET /customers?full_name=John
GET /customers?is_active=trueRange 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]=18Operators: gte, gt, lte, lt
Array Filtering (IN Query)
Filter by multiple values:
GET /customers?status[]=pending&status[]=approved
GET /customers?status=pending,approvedNested Relation Filtering
Filter by related entity fields:
GET /customers?shop.id=10
GET /customers?shop.name=MyShopPagination
GET /customers?page=2&limit=20page: Page number (default: 1)limit: Items per page (default: 10, max: 100)
Sorting
New Multi-field Syntax (Recommended)
GET /customers?sort=name,-createdAtname→ 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=descMulti-field:
GET /customers?sortBy=createdAt,firstName&sortOrder=desc,ascsortBy: Comma-separated fields to sort bysortOrder: Comma-separated order values (ascordesc), defaults toasc
Combined Example
GET /customers?searchTerm=john&status[]=active&age[gte]=18&page=1&limit=20&sort=name,-createdAtThis 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 numberDecorators
@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:
- Single Query Parse - Uses
qs.parsewithallowDots: trueto parse the query string only once - O(1) Field Lookups - Uses
Setfor field lookups instead of array includes - Modular Architecture - Separates concerns into dedicated parsers
- 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:
- Global Configuration (
SmartQueryModule.forRoot()) - System-level settings that apply globally - 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.
