vanta-api
v1.5.0
Published
Advanced API features and security configuration for Node.js/MongoDB.
Maintainers
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,$norfilter support - Automatic
ObjectIdconversion - 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-apiRequired 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?
addManualFilters()adds backend-controlled filters.filter()creates the base$match.populate()joins referenced documents.search()searches normal or populated fields.sort()sorts final results.limitFields()controls output fields.paginate()applies paging.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=phoneGenerated filter:
{
category: "phone"
}Comparison Operators
GET /api/products?price[gte]=100&price[lte]=500Generated filter:
{
price: {
$gte: 100,
$lte: 500
}
}Boolean / Null / Number Conversion
GET /api/products?isActive=true&deletedAt=null&price=100Generated 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=iphoneconst 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,categoryor:
.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,-passwordThrows:
Cannot mix include and exclude fieldspaginate()
Adds $skip and $limit.
GET /api/products?page=2&limit=10Pipeline:
[
{ $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=userMultiple 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.jsYou 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=iphoneconst 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,-passwordpaginate()
برای صفحهبندی استفاده میشود.
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.jsexport 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
