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

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

Readme

mongoose-filter-kit

npm version License: MIT TypeScript

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-kit

Mongoose 6.x or higher is required as a peer dependency:

npm install mongoose

JavaScript 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=25
const 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:

  1. Reserved params are never treated as field filterspage, limit, sort, order, search, from, to, populate, and select are always consumed by their dedicated handlers.

  2. A field key is accepted only when:

    • It appears in your allowedFilters array, or
    • It exists as a path in model.schema.paths.

    Unknown keys with no schema definition are silently dropped.

  3. 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.
  4. customFilter is 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