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

vanta-api

v1.5.0

Published

Advanced API features and security configuration for Node.js/MongoDB.

Readme

🚀 Vanta API

Vanta API is a lightweight, reusable API utility toolkit for Express.js and Mongoose applications.

It helps you build clean, secure, and production-friendly API endpoints with advanced filtering, search, sorting, pagination, populate, centralized error handling, and async route handling.


✨ Features

  • Advanced query filtering
  • Manual server-side filters
  • Recursive $and, $or, $nor filter support
  • Automatic ObjectId conversion
  • Search using q
  • Case-insensitive regex search
  • Sorting
  • Field limiting / projection
  • Pagination
  • Aggregation-based populate
  • Nested populate support
  • Role-based security limits
  • Forbidden field protection
  • Async route wrapper
  • Centralized Express error handler
  • Custom operational error class

📦 Installation

npm install vanta-api

Required dependencies in your app:

npm install express mongoose

📁 Project Files

src/
  api-features.js
  catchAsync.js
  config.js
  errorHandler.js
  handleError.js
  security-default-config.js

| File | Purpose | |---|---| | api-features.js | Main API query builder for filtering, search, sorting, pagination, populate, and execution | | catchAsync.js | Wraps async Express route handlers and forwards errors to next() | | errorHandler.js | Global Express error middleware | | handleError.js | Custom operational error class | | config.js | Loads and merges security configuration | | security-default-config.js | Default security rules and role-based limits |


🇬🇧 English Documentation

Quick Start

import ApiFeatures,{catchAsync,catchError,HandleERROR} from "vanta-api";

Example controller:

export const getProducts = catchAsync(async (req, res, next) => {
  const result = await new ApiFeatures(Product, req.query, req.user?.role)
    .filter()
    .search(["name", "description"])
    .sort()
    .limitFields()
    .paginate()
    .execute();

  res.status(200).json(result);
});

ApiFeatures

ApiFeatures is the main class of this package. It converts request query parameters and manual backend filters into a MongoDB aggregation pipeline.

Constructor

new ApiFeatures(model, query, userRole)

| Parameter | Type | Required | Description | |---|---|---|---| | model | Mongoose Model | Yes | The Mongoose model used to run aggregation | | query | Object | No | Usually req.query | | userRole | String | No | Role name used for security rules |

Example:

const features = new ApiFeatures(Product, req.query, req.user?.role);

If userRole is missing or invalid, the default role is guest.


Recommended Chain Order

const result = await new ApiFeatures(Model, req.query, req.user?.role)
  .addManualFilters(serverSideFilters)
  .filter()
  .populate(populateOptions)
  .search(["name", "description"])
  .sort()
  .limitFields()
  .paginate()
  .execute();

Why this order?

  1. addManualFilters() adds backend-controlled filters.
  2. filter() creates the base $match.
  3. populate() joins referenced documents.
  4. search() searches normal or populated fields.
  5. sort() sorts final results.
  6. limitFields() controls output fields.
  7. paginate() applies paging.
  8. execute() runs the aggregation.

filter()

Builds a MongoDB $match stage from req.query.

new ApiFeatures(Product, req.query)
  .filter()
  .execute();

Simple Filter

GET /api/products?category=phone

Generated filter:

{
  category: "phone"
}

Comparison Operators

GET /api/products?price[gte]=100&price[lte]=500

Generated filter:

{
  price: {
    $gte: 100,
    $lte: 500
  }
}

Boolean / Null / Number Conversion

GET /api/products?isActive=true&deletedAt=null&price=100

Generated values:

{
  isActive: true,
  deletedAt: null,
  price: 100
}

Strings with leading zero are preserved:

GET /api/users?code=0012
{
  code: "0012"
}

ObjectId Conversion

Fields like _id, id, and fields ending with id are converted to ObjectId when the value is a strict MongoDB ObjectId.

GET /api/products?_id=665f0f6f4e7d9a2e2c123456
{
  _id: ObjectId("665f0f6f4e7d9a2e2c123456")
}

Reserved query keys are excluded from normal filtering:

["page", "limit", "sort", "fields", "populate", "q"]

addManualFilters(filters)

Adds backend-controlled filters manually. This is useful when you want to enforce conditions that users should not control from the URL.

new ApiFeatures(Order, req.query)
  .addManualFilters({ user: req.user._id })
  .filter()
  .execute();

$and Example

const result = await new ApiFeatures(Product, req.query)
  .addManualFilters({
    $and: [
      { _id: "665f0f6f4e7d9a2e2c123456" },
      { isActive: true }
    ]
  })
  .filter()
  .execute();

The _id inside $and is recursively converted to ObjectId.

$or Example

const result = await new ApiFeatures(Product, req.query)
  .addManualFilters({
    $or: [
      { ownerId: "665f0f6f4e7d9a2e2c123456" },
      { createdById: "665f0f6f4e7d9a2e2c654321" }
    ]
  })
  .filter()
  .execute();

$nor Example

const result = await new ApiFeatures(Product, req.query)
  .addManualFilters({
    $nor: [
      { status: "blocked" },
      { isDeleted: true }
    ]
  })
  .filter()
  .execute();

$in Example

const result = await new ApiFeatures(Product, req.query)
  .addManualFilters({
    _id: {
      $in: [
        "665f0f6f4e7d9a2e2c123456",
        "665f0f6f4e7d9a2e2c654321"
      ]
    }
  })
  .filter()
  .execute();

search(fields)

Searches using the q key from req.query. It always uses case-insensitive regex search.

.search(["name", "description"])
GET /api/products?q=iphone
const result = await new ApiFeatures(Product, req.query)
  .filter()
  .search(["name", "description", "brand"])
  .paginate()
  .execute();

Generated condition:

{
  $or: [
    { name: { $regex: "iphone", $options: "i" } },
    { description: { $regex: "iphone", $options: "i" } },
    { brand: { $regex: "iphone", $options: "i" } }
  ]
}

q is reserved and is not treated as a normal filter.


sort()

Sorts results using the sort query key.

GET /api/products?sort=-createdAt,price
{
  createdAt: -1,
  price: 1
}

Only fields existing in the model schema are accepted.


limitFields(input)

Controls returned fields using projection.

GET /api/products?fields=name,price,category

or:

.limitFields("name,price")

Include mode:

{ name: 1, price: 1 }

Exclude mode:

GET /api/products?fields=-description
{ description: 0 }

Mixed include/exclude is not allowed:

GET /api/products?fields=name,-password

Throws:

Cannot mix include and exclude fields

paginate()

Adds $skip and $limit.

GET /api/products?page=2&limit=10

Pipeline:

[
  { $skip: 10 },
  { $limit: 10 }
]

Limits are capped by role.


populate(input)

Performs aggregation-based populate using $lookup.

.populate("user")

From query:

GET /api/posts?populate=user

Multiple populate paths:

.populate(["user", "category"])

Nested populate:

.populate({
  path: "user",
  populate: {
    path: "company",
    populate: {
      path: "country"
    }
  }
})

Dot notation:

.populate("user.company.country")

Populate with select:

.populate({
  path: "user",
  select: "name email"
})

Exclude fields:

.populate({
  path: "user",
  select: "-password"
})

Mixed include/exclude is not allowed.


execute(options)

Runs the aggregation pipeline.

const result = await features.execute();

Returns:

{
  success: true,
  count: 25,
  data: [...]
}

Options:

.execute({
  debug: true,
  useCursor: false,
  batchSize: 100,
  maxTimeMS: 10000,
  allowDiskUse: true,
  readConcern: "majority"
})

| Option | Type | Default | Description | |---|---|---|---| | debug | Boolean | false | Logs the pipeline | | useCursor | Boolean | false | Uses aggregation cursor | | batchSize | Number | 100 | Cursor batch size | | maxTimeMS | Number | 10000 | Max execution time | | allowDiskUse | Boolean | false | Allows disk usage | | readConcern | String | majority | MongoDB read concern |


Full Controller Example

import ApiFeatures,{catchAsync} from "vanta-api";
import Product from "../models/productModel.js";

export const getProducts = catchAsync(async (req, res, next) => {
  const result = await new ApiFeatures(Product, req.query, req.user?.role)
    .addManualFilters({ isDeleted: false })
    .filter()
    .populate([
      {
        path: "category",
        select: "name slug"
      },
      {
        path: "createdBy",
        select: "name email"
      }
    ])
    .search(["name", "description", "category.name"])
    .sort()
    .limitFields()
    .paginate()
    .execute();

  res.status(200).json(result);
});

Error Handling

HandleERROR

Custom operational error class.

import {HandleERROR} from "vanta-api";

throw new HandleERROR("Product not found", 404);

Example properties:

{
  message: "Product not found",
  statusCode: 404,
  status: "fail",
  isOperational: true
}

For 4xx errors, status is fail. For 5xx errors, status is error.

catchAsync

Wraps async Express handlers and removes repetitive try/catch.

import {catchAsync} from "vanta-api";

app.get(
  "/products",
  catchAsync(async (req, res, next) => {
    const products = await Product.find();
    res.json(products);
  })
);

Any rejected promise is forwarded to Express next().

errorHandler

Global Express error middleware.

import {catchError} from "vanta-api";

app.use(catchError);

Example response:

{
  "status": "fail",
  "success": false,
  "message": "Product not found"
}

Use it after all routes:

app.use("/api/products", productRouter);
app.use(catchError);

Security Configuration

Default security settings are stored in:

src/security-default-config.js

You can override them by creating a security-config.js file in your project root.

export const securityConfig = {
  allowedOperators: [
    "eq",
    "ne",
    "gt",
    "gte",
    "lt",
    "lte",
    "in",
    "nin",
    "regex",
    "exists"
  ],

  forbiddenFields: ["password", "refreshToken", "resetPasswordToken"],

  maxPipelineStages: 50,

  accessLevels: {
    guest: {
      maxLimit: 20,
      allowedPopulate: ["category"]
    },
    user: {
      maxLimit: 100,
      allowedPopulate: ["category", "createdBy"]
    },
    admin: {
      maxLimit: 1000,
      allowedPopulate: ["*"]
    }
  }
};

Express Setup Example

import express from "express";
import {catchError} from "vanta-api";
import productRouter from "./routes/productRoutes.js";

const app = express();

app.use(express.json());
app.use("/api/products", productRouter);
app.use(catchError);

export default app;

🇮🇷 مستندات فارسی

شروع سریع

import ApiFeatures,{catchAsync,catchError,HandleERROR} from "vanta-api";

مثال controller:

export const getProducts = catchAsync(async (req, res, next) => {
  const result = await new ApiFeatures(Product, req.query, req.user?.role)
    .filter()
    .search(["name", "description"])
    .sort()
    .limitFields()
    .paginate()
    .execute();

  res.status(200).json(result);
});

ApiFeatures چیست؟

ApiFeatures کلاس اصلی پکیج است. این کلاس از روی req.query و فیلترهای دستی سمت سرور، یک MongoDB aggregation pipeline می‌سازد.

Constructor

new ApiFeatures(model, query, userRole)

| ورودی | نوع | اجباری | توضیح | |---|---|---|---| | model | Mongoose Model | بله | مدلی که aggregation روی آن اجرا می‌شود | | query | Object | خیر | معمولاً همان req.query | | userRole | String | خیر | نقش کاربر برای قوانین امنیتی |

اگر userRole داده نشود یا معتبر نباشد، role پیش‌فرض guest استفاده می‌شود.


ترتیب پیشنهادی استفاده

const result = await new ApiFeatures(Model, req.query, req.user?.role)
  .addManualFilters(serverSideFilters)
  .filter()
  .populate(populateOptions)
  .search(["name", "description"])
  .sort()
  .limitFields()
  .paginate()
  .execute();

filter()

از روی req.query مرحله‌ی $match می‌سازد.

GET /api/products?category=phone
{
  category: "phone"
}

operatorهای مقایسه‌ای:

GET /api/products?price[gte]=100&price[lte]=500
{
  price: {
    $gte: 100,
    $lte: 500
  }
}

تبدیل‌های خودکار:

GET /api/products?isActive=true&deletedAt=null&price=100
{
  isActive: true,
  deletedAt: null,
  price: 100
}

کلیدهایی مثل _id, id و کلیدهایی که به id ختم می‌شوند، اگر مقدارشان ObjectId معتبر باشد، به ObjectId تبدیل می‌شوند.


addManualFilters(filters)

برای اضافه کردن فیلترهای دستی سمت سرور استفاده می‌شود.

new ApiFeatures(Order, req.query)
  .addManualFilters({ user: req.user._id })
  .filter()
  .execute();

استفاده از $and:

const result = await new ApiFeatures(Product, req.query)
  .addManualFilters({
    $and: [
      { _id: "665f0f6f4e7d9a2e2c123456" },
      { isActive: true }
    ]
  })
  .filter()
  .execute();

در این حالت _id داخل $and هم به صورت recursive به ObjectId تبدیل می‌شود.

استفاده از $or:

const result = await new ApiFeatures(Product, req.query)
  .addManualFilters({
    $or: [
      { ownerId: "665f0f6f4e7d9a2e2c123456" },
      { createdById: "665f0f6f4e7d9a2e2c654321" }
    ]
  })
  .filter()
  .execute();

استفاده از $in:

const result = await new ApiFeatures(Product, req.query)
  .addManualFilters({
    _id: {
      $in: [
        "665f0f6f4e7d9a2e2c123456",
        "665f0f6f4e7d9a2e2c654321"
      ]
    }
  })
  .filter()
  .execute();

search(fields)

از کلید q داخل req.query مقدار را می‌گیرد و با regex جستجو می‌کند. جستجو همیشه case-insensitive است.

GET /api/products?q=iphone
const result = await new ApiFeatures(Product, req.query)
  .filter()
  .search(["name", "description", "brand"])
  .paginate()
  .execute();

نکته: q جزو کلیدهای رزرو شده است و وارد filter معمولی نمی‌شود.


sort()

از sort داخل query برای مرتب‌سازی استفاده می‌کند.

GET /api/products?sort=-createdAt,price
{
  createdAt: -1,
  price: 1
}

limitFields(input)

برای کنترل فیلدهای خروجی استفاده می‌شود.

GET /api/products?fields=name,price,category

یا مستقیم:

.limitFields("name,price")

حالت include:

{ name: 1, price: 1 }

حالت exclude:

{ description: 0 }

حالت mixed مجاز نیست:

GET /api/products?fields=name,-password

paginate()

برای صفحه‌بندی استفاده می‌شود.

GET /api/products?page=2&limit=10
[
  { $skip: 10 },
  { $limit: 10 }
]

populate(input)

با استفاده از aggregation و $lookup داده‌های مرتبط را join می‌کند.

.populate("user")

از query:

GET /api/posts?populate=user

چند populate:

.populate(["user", "category"])

nested populate:

.populate({
  path: "user",
  populate: {
    path: "company",
    populate: {
      path: "country"
    }
  }
})

یا با dot notation:

.populate("user.company.country")

select در populate:

.populate({
  path: "user",
  select: "name email"
})

exclude:

.populate({
  path: "user",
  select: "-password"
})

execute(options)

pipeline را اجرا می‌کند.

const result = await features.execute();

خروجی:

{
  success: true,
  count: 25,
  data: [...]
}

مدیریت خطاها

HandleERROR

کلاس خطای اختصاصی.

throw new HandleERROR("Product not found", 404);
{
  message: "Product not found",
  statusCode: 404,
  status: "fail",
  isOperational: true
}

catchAsync

برای حذف try/catch تکراری در route handlerهای async.

app.get(
  "/products",
  catchAsync(async (req, res, next) => {
    const products = await Product.find();
    res.json(products);
  })
);

errorHandler

middleware مرکزی خطاها:

app.use(catchError);

نمونه خروجی:

{
  "status": "fail",
  "success": false,
  "message": "Product not found"
}

تنظیمات امنیتی

برای override کردن تنظیمات پیش‌فرض، در root پروژه فایل زیر را بسازید:

security-config.js
export const securityConfig = {
  allowedOperators: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "regex", "exists"],
  forbiddenFields: ["password", "refreshToken", "resetPasswordToken"],
  maxPipelineStages: 50,
  accessLevels: {
    guest: {
      maxLimit: 20,
      allowedPopulate: ["category"]
    },
    user: {
      maxLimit: 100,
      allowedPopulate: ["category", "createdBy"]
    },
    admin: {
      maxLimit: 1000,
      allowedPopulate: ["*"]
    }
  }
};

License

MIT