mongoose-filter-kit
v1.0.0
Published
Mongoose query builder with search, pagination, sorting, date filters, populate and dynamic filters for REST APIs and admin panels
Maintainers
Readme
mongoose-filter-kit
A production-ready Mongoose query builder for REST APIs and admin panels. One function call builds a full MongoDB query with regex search, pagination, sorting, date-range filters, populate, field selection, and dynamic filters — all driven by URL query parameters.
Install
npm install mongoose-filter-kitMongoose 6.x or higher is required as a peer dependency:
npm install mongooseJavaScript Usage
CommonJS (Node.js require)
const { queryBuilder } = require('mongoose-filter-kit');
const User = require('./models/User');
app.get('/users', async (req, res) => {
const result = await queryBuilder({
model: User,
query: req.query,
searchFields: ['name', 'email'],
allowedFilters: ['role', 'status'],
customFilter: { isDeleted: false },
});
res.json(result);
});ESM (import)
import { queryBuilder } from 'mongoose-filter-kit';
import User from './models/User.js';
app.get('/users', async (req, res) => {
const result = await queryBuilder({
model: User,
query: req.query,
searchFields: ['name', 'email'],
allowedFilters: ['role', 'status'],
customFilter: { isDeleted: false },
});
res.json(result);
});Quick Start (TypeScript)
import { queryBuilder } from 'mongoose-filter-kit';
import User from './models/User';
// Express handler
app.get('/users', async (req, res) => {
const result = await queryBuilder({
model: User,
query: req.query,
searchFields: ['name', 'email'],
allowedFilters: ['role', 'status'],
defaultSort: 'createdAt',
defaultOrder: 'desc',
defaultLimit: 10,
maxLimit: 100,
populate: 'role',
dateField: 'createdAt',
customFilter: { isDeleted: false },
});
res.json(result);
});Sample response:
{
"data": [
{ "_id": "...", "name": "John Doe", "email": "[email protected]", "role": "admin" }
],
"pagination": {
"total": 42,
"page": 1,
"limit": 10,
"pages": 5,
"hasNext": true,
"hasPrev": false
}
}Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
| model | Model<T> | required | The Mongoose model to query against. |
| query | Record<string, any> | required | Parsed query string object (e.g. req.query). |
| searchFields | string[] | undefined | Fields to regex-search when ?search= is provided. |
| allowedFilters | string[] | undefined | Extra query keys allowed as dynamic filters (in addition to schema paths). |
| defaultSort | string | 'createdAt' | Default sort field when ?sort= is absent. |
| defaultOrder | 'asc' \| 'desc' | 'desc' | Default sort direction when ?order= is absent. |
| defaultLimit | number | 10 | Default page size when ?limit= is absent. |
| maxLimit | number | 100 | Hard ceiling on page size — prevents abuse. |
| populate | string \| string[] \| PopulateOptions \| PopulateOptions[] | undefined | Populate option used when ?populate= is absent. |
| select | ProjectionType<T> \| string | undefined | Field projection used when ?select= is absent. |
| dateField | string | undefined | Date field targeted by ?from= / ?to= filters. |
| lean | boolean | false | Return plain JS objects instead of Mongoose documents. |
| customFilter | Record<string, any> | undefined | Extra conditions always merged into the query via $and. |
Query Parameters Reference
| Param | Type | Example | Description |
|---|---|---|---|
| search | string | ?search=john | Case-insensitive regex search across all searchFields. Special regex chars are escaped automatically. |
| page | number | ?page=2 | Page number (1-based, min 1). |
| limit | number | ?limit=25 | Documents per page (clamped to maxLimit). |
| sort | string | ?sort=name | Field to sort by. |
| order | 'asc' \| 'desc' | ?order=asc | Sort direction. |
| from | ISO 8601 date | ?from=2024-01-01 | Start of date range on dateField ($gte). Invalid dates are silently ignored. |
| to | ISO 8601 date | ?to=2024-12-31 | End of date range on dateField ($lte, adjusted to 23:59:59.999). Invalid dates are silently ignored. |
| populate | string | ?populate=role,department | Comma-separated paths to populate. Overrides the populate option. |
| select | string | ?select=name,email | Comma-separated fields to include. Overrides the select option. |
| <field> | string | ?status=active | Dynamic filter — passed through only if the key is in allowedFilters or exists as a schema path. Comma-separated values become { $in: [...] }. The strings 'true' / 'false' are coerced to booleans. |
Examples
With Express
import express from 'express';
import mongoose from 'mongoose';
import { queryBuilder } from 'mongoose-filter-kit';
import Product from './models/Product';
const app = express();
app.get('/products', async (req, res, next) => {
try {
const result = await queryBuilder({
model: Product,
query: req.query,
searchFields: ['name', 'description', 'sku'],
allowedFilters: ['category', 'brand', 'inStock'],
defaultSort: 'createdAt',
defaultOrder: 'desc',
defaultLimit: 20,
maxLimit: 200,
dateField: 'createdAt',
populate: [{ path: 'category', select: 'name slug' }],
customFilter: { isActive: true },
lean: true,
});
res.json(result);
} catch (err) {
next(err);
}
});With NestJS
import { Controller, Get, Query } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { queryBuilder, QueryBuilderResult } from 'mongoose-filter-kit';
import { User, UserDocument } from './user.schema';
@Controller('users')
export class UsersController {
constructor(
@InjectModel(User.name) private readonly userModel: Model<UserDocument>,
) {}
@Get()
async findAll(@Query() query: Record<string, any>): Promise<QueryBuilderResult<UserDocument>> {
return queryBuilder({
model: this.userModel,
query,
searchFields: ['name', 'email'],
allowedFilters: ['role', 'status', 'department'],
defaultSort: 'createdAt',
defaultOrder: 'desc',
defaultLimit: 10,
maxLimit: 100,
populate: 'role',
dateField: 'createdAt',
customFilter: { isDeleted: false },
});
}
}With Custom Filters and Populate Options
const result = await queryBuilder({
model: Order,
query: req.query,
searchFields: ['orderNumber', 'customerName'],
allowedFilters: ['status', 'paymentMethod'],
defaultSort: 'placedAt',
defaultOrder: 'desc',
defaultLimit: 15,
maxLimit: 50,
dateField: 'placedAt',
populate: [
{ path: 'customer', select: 'name email phone' },
{ path: 'items.product', select: 'name sku price' },
],
select: '-__v -internalNotes',
customFilter: { archived: false },
lean: true,
});Combining Date Ranges and Boolean Filters
GET /orders?from=2024-01-01&to=2024-03-31&status=pending,processing&isPaid=false&page=1&limit=25const result = await queryBuilder({
model: Order,
query: req.query, // { from, to, status, isPaid, page, limit }
allowedFilters: ['isPaid'],
dateField: 'placedAt',
// status is also allowed because it exists on the schema
});
// Produces: { $and: [
// { placedAt: { $gte: ..., $lte: ... } },
// { status: { $in: ['pending', 'processing'] }, isPaid: false }
// ]}How Dynamic Filters Work
Dynamic filters let clients filter by any model field directly via the URL — but with a security gate to prevent arbitrary injection:
Reserved params are never treated as field filters —
page,limit,sort,order,search,from,to,populate, andselectare always consumed by their dedicated handlers.A field key is accepted only when:
- It appears in your
allowedFiltersarray, or - It exists as a path in
model.schema.paths.
Unknown keys with no schema definition are silently dropped.
- It appears in your
Value coercions applied automatically:
?status=active,inactive→{ status: { $in: ['active', 'inactive'] } }?isVerified=true→{ isVerified: true }(boolean)?isVerified=false→{ isVerified: false }(boolean)- Everything else is passed through as-is.
customFilteris always applied — it cannot be overridden by query parameters. Use it for tenant isolation, soft-delete guards, or any condition that must always be present.
TypeScript Support
Full types ship with the package — no @types/ install needed. All types are exported:
import {
queryBuilder,
QueryBuilderOptions,
QueryBuilderResult,
PaginationMeta,
} from 'mongoose-filter-kit';The main function is generic — pass your document type for full type inference:
const result = await queryBuilder<UserDocument>({
model: User,
query: req.query,
});
// result.data is UserDocument[]License
MIT
