mongoose-query-kit
v3.0.1
Published
Reusable Mongoose query handler for search, sort, pagination, and filtering.
Maintainers
Readme
Mongoose Query Kit
Powerful query builder classes (FindQuery and AggregationQuery) that enhance Mongoose queries with clean chaining for pagination, filtering, searching, sorting, field selection, statistics collection, and lean query support.
✨ Features
- ✅ Clean chainable API
- 🔍 Search by specific fields
- 📑 Pagination support
- 🔢 Filtering by any query field (supports
$orand$andoperators) - ↕️ Sorting
- 🔐 Field selection
- 🔗 Document population (same
PopulateOptionsAPI for both FindQuery and AggregationQuery) - 🪶 Lean query support
- 📊 Count-only query support
- 📈 Statistics collection (collect multiple counts in a single query)
- 🗑️ Automatic soft-delete handling (
is_deletedfield) - 🔗 Seamless frontend-driven query support (perfect for single-endpoint APIs)
- 🧠 Full TypeScript support
- 🔄 Two Query Builders (FindQuery for find operations, AggregationQuery for aggregation pipelines)
📦 Installation
npm install mongoose-query-kit
# or
yarn add mongoose-query-kit
# or
pnpm add mongoose-query-kit🚀 Migration to v3.0
Breaking Changes
v3.0.0 introduces major breaking changes:
Class Name Changed:
MongooseQuery→FindQuery(for find-based queries)- New:
AggregationQuery(for aggregation-based queries)
Import Changes:
// Before (v2.x) import { MongooseQuery } from 'mongoose-query-kit'; // After (v3.0.0) import { FindQuery, AggregationQuery } from 'mongoose-query-kit';Constructor Changes:
// Before (v2.x) new MongooseQuery(UserModel.find(), req.query) // After (v3.0.0) - FindQuery new FindQuery(UserModel, req.query) // After (v3.0.0) - AggregationQuery new AggregationQuery(UserModel, req.query)
Migration Steps
Update Imports:
// Change from import { MongooseQuery } from 'mongoose-query-kit'; // To import { FindQuery } from 'mongoose-query-kit';Update Class Usage:
// Change from new MongooseQuery(UserModel.find(), req.query) // To new FindQuery(UserModel, req.query)Choose the Right Query Builder:
- Use
FindQueryfor standard find-based queries (same as v2.x) - Use
AggregationQueryfor complex aggregation pipelines with custom stages
- Use
New Features in v3.0.0
- ✅ Two Query Builders:
FindQueryfor find-based queries andAggregationQueryfor aggregation pipelines - ✅ Pipeline Support:
AggregationQueryincludespipeline()method for custom aggregation stages - ✅ Statistics Collection: Collect multiple counts in a single query
- ✅ OR/AND Filter Support: Complex filtering with
$orand$andoperators - ✅ Automatic Soft Delete: Handles
is_deletedfield automatically (FindQuery only) - ✅ Simplified API: Pass Model directly instead of
Model.find()
🔗 Frontend-Driven Queries (Single API Endpoint Design)
Using FindQuery or AggregationQuery allows your frontend to send query parameters directly, enabling dynamic filtering, pagination, searching, and sorting—all through a single API endpoint.
This design pattern makes your backend flexible and minimizes code repetition.
✅ Benefits
- One endpoint, multiple use cases
- Query operations controlled dynamically from frontend
- Backend can selectively enable/disable operations via method chaining
- Great for dashboards, admin panels, and advanced filtering systems
📲 Example: Frontend → Backend
Frontend Code
const query = new URLSearchParams({
page: '1',
limit: '10',
search: 'john',
sort: '-createdAt',
fields: 'name,email',
status: 'active',
}).toString();
fetch(`/api/users?${query}`);Backend Code
import { FindQuery } from 'mongoose-query-kit';
import UserModel from '../models/user.model';
const getUsers = async (req, res) => {
const searchableFields = ['name', 'email'];
const filterableFields = ['status', 'role'];
const sortableFields = ['name', 'email', 'createdAt'];
const result = await new FindQuery(UserModel, req.query)
.search(searchableFields)
.filter(filterableFields)
.sort(sortableFields)
.fields()
.paginate()
.tap((q) => q.lean())
.execute([
{ key: 'active', filter: { status: 'active' } },
{ key: 'inactive', filter: { status: 'inactive' } },
]);
res.json(result);
};Complete Real-World Example
import { FindQuery } from 'mongoose-query-kit';
import UserModel from '../models/user.model';
const getUsersWithStats = async (req, res) => {
try {
const result = await new FindQuery(UserModel, req.query)
.search(['name', 'email']) // Allow search on name and email
.filter(['status', 'role', 'verified']) // Only allow these filters
.sort(['name', 'createdAt', 'email']) // Only allow sorting by these fields
.fields(['name', 'email', 'status', 'role']) // Only allow selecting these fields
.paginate()
.tap((q) => q.lean())
.execute([
// Collect statistics
{ key: 'totalActive', filter: { status: 'active' } },
{ key: 'totalVerified', filter: { verified: true } },
{ key: 'totalAdmins', filter: { role: 'admin' } },
]);
res.json({
success: true,
...result,
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
};🧠 Usage
FindQuery vs AggregationQuery
FindQuery - Use for standard find-based queries (same as v2.x):
- Based on Mongoose
find()method - Returns Mongoose Documents
- Supports
.lean()viatap() - Uses Mongoose's native
populate()method - Best for simple queries and CRUD operations
AggregationQuery - Use for complex aggregation pipelines:
- Based on Mongoose
aggregate()method - Returns plain objects
- Supports custom pipeline stages via
pipeline()method - Converts
populate()to$lookupaggregation stages internally - Best for complex data transformations, joins, and aggregations
Common API:
- Both use the same
PopulateOptionsformat forpopulate()method - Same method signatures for
search(),filter(),sort(),paginate(),fields(),tap(),execute() - Consistent API makes it easy to switch between FindQuery and AggregationQuery
FindQuery - Basic Example
import { FindQuery } from 'mongoose-query-kit';
const result = await new FindQuery(UserModel, req.query)
.search(['name', 'email'])
.filter()
.sort()
.fields()
.paginate()
.tap((q) => q.lean())
.execute();AggregationQuery - Basic Example
import { AggregationQuery } from 'mongoose-query-kit';
const result = await new AggregationQuery(UserModel, req.query)
.search(['name', 'email'])
.filter()
.sort()
.fields()
.paginate()
.execute();AggregationQuery - With Custom Pipeline Stages
import { AggregationQuery } from 'mongoose-query-kit';
const result = await new AggregationQuery(UserModel, req.query)
.filter()
.pipeline([
{ $addFields: { nameUpper: { $toUpper: '$name' } } },
{ $lookup: { from: 'posts', localField: '_id', foreignField: 'author', as: 'posts' } }
])
.sort(['name'])
.paginate()
.execute();FindQuery - With Populate
import { FindQuery } from 'mongoose-query-kit';
// Both string and PopulateOptions work
const result = await new FindQuery(PostModel, req.query)
.filter()
.populate('author') // Simple string
.populate({ // Or PopulateOptions object
path: 'comments',
select: 'text createdAt',
match: { approved: true },
populate: {
path: 'author',
select: 'name',
},
})
.paginate()
.execute();AggregationQuery - With Populate
import { AggregationQuery } from 'mongoose-query-kit';
// Same API format as FindQuery!
const result = await new AggregationQuery(PostModel, req.query)
.filter()
.populate('author') // Simple string (uses schema ref)
.populate({ // Or PopulateOptions object
path: 'comments',
select: 'text createdAt',
match: { approved: true },
})
.sort(['createdAt'])
.paginate()
.execute();Note: Both FindQuery and AggregationQuery use the same PopulateOptions format for consistency. FindQuery uses Mongoose's native populate, while AggregationQuery converts to $lookup stages internally.
Count Only Query
If your query includes is_count_only=true, both query builders will return only the total count in the response, skipping data fetching for performance.
// Query string: ?is_count_only=true
const result = await new FindQuery(UserModel, req.query)
.filter()
.execute();
// Result:
{
data: [],
meta: {
total: 143,
page: 1,
limit: 0
}
}Statistics Collection
Collect multiple counts in a single query. Perfect for dashboards and analytics.
const result = await new FindQuery(UserModel, req.query)
.filter()
.execute([
{ key: 'active', filter: { status: 'active' } },
{ key: 'pending', filter: { status: 'pending' } },
{ key: 'blocked', filter: { status: 'blocked' } },
]);
// Result:
{
data: [...],
meta: {
total: 150,
page: 1,
limit: 10,
statistics: {
active: 120,
pending: 20,
blocked: 10
}
}
}Advanced Filtering with OR/AND
Support for complex MongoDB queries using $or and $and operators.
// Query string: ?or[0][status]=active&or[1][role]=admin
const result = await new FindQuery(UserModel, req.query)
.filter()
.execute();
// This will create: { $or: [{ status: 'active' }, { role: 'admin' }] }Soft Delete Handling
Automatically excludes documents where is_deleted: true unless explicitly included in the filter (FindQuery only).
// Automatically filters out deleted items
const result = await new FindQuery(UserModel, req.query)
.filter()
.execute();
// To include deleted items, explicitly set is_deleted in query params
// Query string: ?is_deleted=true📦 API Methods
Common Methods (Both FindQuery & AggregationQuery)
| Method | Description |
| ------------ | -------------------------------------------------------------------------- |
| search() | Enables fuzzy search on specified fields using search query parameter |
| filter() | Applies filtering using query key-value pairs (supports $or and $and) |
| sort() | Sorts results, e.g. ?sort=name or ?sort=-createdAt |
| fields() | Selects fields to include, e.g. ?fields=name,email |
| paginate() | Adds pagination via ?page=1&limit=10 |
| populate() | Populates referenced documents. Both use same PopulateOptions format (FindQuery uses native populate, AggregationQuery uses $lookup) |
| tap() | Provides direct access to modify the query/pipeline |
| execute() | Runs the query, returns result and meta info. Accepts optional statistics |
AggregationQuery Only
| Method | Description |
| ------------ | -------------------------------------------------------------------------- |
| pipeline() | Adds custom aggregation pipeline stages to the query |
Method Details
search(applicableFields: (keyof T)[])
Enables case-insensitive regex search on specified fields.
Query Parameters:
search: The search term
Example:
// Query: ?search=john
new FindQuery(UserModel, req.query)
.search(['name', 'email'])filter(applicableFields?: (keyof T)[])
Applies filtering from query parameters. If applicableFields is provided, only those fields will be allowed.
Supports:
- Simple key-value pairs:
?status=active&role=admin $oroperator:?or[0][status]=active&or[1][role]=admin$andoperator:?and[0][status]=active&and[1][verified]=true
Example:
// Query: ?status=active&role=admin
new FindQuery(UserModel, req.query)
.filter(['status', 'role']) // Only allow status and role filterssort(applicableFields?: (keyof T)[])
Sorts results. Defaults to -createdAt if no sort is specified.
Query Parameters:
sort: Comma-separated fields, prefix with-for descending
Example:
// Query: ?sort=-createdAt,name
new FindQuery(UserModel, req.query)
.sort(['name', 'createdAt', 'email']) // Only allow these fieldsfields(applicableFields?: (keyof T)[])
Selects which fields to return. Defaults to all fields except __v.
Query Parameters:
fields: Comma-separated field names
Example:
// Query: ?fields=name,email
new FindQuery(UserModel, req.query)
.fields(['name', 'email', 'createdAt']) // Only allow these fieldspaginate()
Adds pagination to the query.
Query Parameters:
page: Page number (default: 1)limit: Items per page
Example:
// Query: ?page=2&limit=20
new FindQuery(UserModel, req.query)
.paginate()populate(populateConfig)
Populates referenced documents. Both FindQuery and AggregationQuery use the same API format - Mongoose's PopulateOptions type.
Parameters:
populateConfig:string | PopulateOptions | Array<string | PopulateOptions>string: Simple path (e.g.,'author')PopulateOptions: Mongoose populate options objectArray: Multiple populate configurations
Note: FindQuery uses Mongoose's native populate, while AggregationQuery converts populate options to $lookup aggregation stages internally.
FindQuery Example:
// Simple string populate
new FindQuery(PostModel, req.query)
.populate('author')
.execute();
// PopulateOptions object
new FindQuery(PostModel, req.query)
.populate({
path: 'author',
select: 'name email',
})
.execute();
// Array-based populate with options
new FindQuery(PostModel, req.query)
.populate([
'author',
{
path: 'comments',
select: 'text createdAt',
match: { approved: true },
},
{
path: 'category',
select: 'name',
populate: {
path: 'parent',
select: 'name',
},
},
])
.execute();AggregationQuery Example:
// Simple string populate (uses schema ref to detect collection)
new AggregationQuery(PostModel, req.query)
.populate('author')
.execute();
// PopulateOptions object (same format as FindQuery)
new AggregationQuery(PostModel, req.query)
.populate({
path: 'author',
select: 'name email',
match: { active: true },
})
.execute();
// Array-based populate (same format as FindQuery)
new AggregationQuery(PostModel, req.query)
.populate([
'author',
{
path: 'comments',
select: 'text createdAt',
match: { approved: true },
},
])
.execute();Key Differences:
- FindQuery: Returns Mongoose Documents, uses native Mongoose populate
- AggregationQuery: Returns plain objects, converts to
$lookupstages internally - Both: Support the same
PopulateOptionsformat for consistency
tap(callback)
Provides direct access to modify the query or pipeline.
FindQuery Example:
new FindQuery(UserModel, req.query)
.tap((q) => q.lean())
.tap((q) => q.populate('author'))AggregationQuery Example:
new AggregationQuery(UserModel, req.query)
.tap((pipeline) => [...pipeline, { $limit: 10 }])pipeline(stages: PipelineStage[], position?: number) (AggregationQuery only)
Adds custom aggregation pipeline stages to the query.
Parameters:
stages: Array of MongoDB aggregation pipeline stagesposition(optional): Position to insert stages. If not provided, inserts before pagination/sort/project stages.
Example:
const result = await new AggregationQuery(UserModel, req.query)
.filter()
.pipeline([
{ $addFields: { nameUpper: { $toUpper: '$name' } } },
{ $lookup: {
from: 'posts',
localField: '_id',
foreignField: 'author',
as: 'posts'
}
}
])
.sort(['name'])
.execute();execute(statisticsQueries?: Array<{key: string, filter: Record<string, any>}>)
Executes the query and returns results with metadata.
Parameters:
statisticsQueries(optional): Array of statistics to collect. Each statistic will count documents matching the base filter + the statistic's filter.
Example:
const result = await new FindQuery(UserModel, req.query)
.filter()
.execute([
{ key: 'active', filter: { status: 'active' } },
{ key: 'inactive', filter: { status: 'inactive' } },
]);🧾 Response Format
Standard Response
By default, the response from execute() looks like this:
{
data: T[],
meta: {
total: number,
page: number,
limit: number
}
}With Statistics
When statistics are provided, the response includes a statistics object:
{
data: T[],
meta: {
total: number,
page: number,
limit: number,
statistics: {
[key: string]: number
}
}
}Count Only Response
If is_count_only=true is passed in the query, the response will be:
{
data: [],
meta: {
total: number,
page: number,
limit: number,
statistics?: {
[key: string]: number
}
}
}Note: Statistics are still collected even when is_count_only=true.
🧪 TypeScript Support
Fully typed with generics for safe usage:
interface User {
name: string;
email: string;
status: 'active' | 'inactive';
}
const result = await new FindQuery<User>(UserModel, req.query)
.filter()
.tap((q) => q.lean())
.execute([
{ key: 'active', filter: { status: 'active' } },
]);
// result.data is typed as User[]
// result.meta.statistics is typed as Record<string, number> | undefined📋 Query Parameters Reference
Supported Query Parameters
| Parameter | Type | Description | Example |
| -------------- | ------- | ---------------------------------------------------------------- | -------------------------- |
| search | string | Search term for fuzzy search | ?search=john |
| sort | string | Sort fields (comma-separated, prefix with - for descending) | ?sort=-createdAt,name |
| page | string | Page number for pagination | ?page=2 |
| limit | string | Items per page | ?limit=20 |
| fields | string | Fields to select (comma-separated) | ?fields=name,email |
| is_count_only| string | Return only count without data (true/false) | ?is_count_only=true |
| or | object | OR conditions (array format) | ?or[0][status]=active |
| and | object | AND conditions (array format) | ?and[0][verified]=true |
| [field] | any | Any field name for direct filtering | ?status=active&role=admin |
Query Parameter Examples
Simple Filtering:
GET /api/users?status=active&role=adminSearch with Pagination:
GET /api/users?search=john&page=1&limit=10&sort=-createdAtField Selection:
GET /api/users?fields=name,email,statusOR Conditions:
GET /api/users?or[0][status]=active&or[1][role]=adminAND Conditions:
GET /api/users?and[0][status]=active&and[1][verified]=trueCount Only:
GET /api/users?is_count_only=true&status=active🧪 Testing
pnpm test🌐 Repository
https://github.com/beendoo/mongoose-query-kit.git
📝 License
MIT © Foysal Ahmed
