OpenAPI v3 compliant REST framework for Node.js, with support for MongoDB and JSON-Schema
npm install arrestA powerful OpenAPI v3.1 compliant REST framework for Node.js with comprehensive MongoDB support, JSON Schema validation, authentication, and authorization. Build production-ready RESTful APIs in minutes with automatic OpenAPI documentation generation.
> Latest Release: v14.1.1 - Stable dependencies with improved documentation



---
---
- ✅ OpenAPI v3.1 Compliant: Automatic OpenAPI specification generation with full v3.1 support including JSON Schema Draft 2020-12
- ✅ MongoDB Integration: Built-in MongoDB operations (CRUD) with advanced querying
- ✅ JSON Schema Validation: Comprehensive input validation using JSON Schema
- ✅ Authentication & Authorization: OAuth2 scopes and CASL ability-based permissions
- ✅ Resource Query Language (RQL): Advanced querying with filtering, sorting, and pagination
- ✅ Express.js Integration: Works seamlessly with existing Express.js applications
- ✅ CSV Export: Built-in CSV export functionality for data endpoints
- ✅ JSON-RPC Support: Dual REST and JSON-RPC endpoint support
- ✅ Pipeline Operations: Complex data processing with pipeline support
- ✅ TypeScript Support: Full TypeScript definitions included
- ✅ Modern ES Modules: Native ESM with Node.js 18+
- Node.js: >= 18.17.0
- MongoDB: 6.x or higher (if using MongoResource)
``bashnpm
npm install arrest
Quick Start
$3
`javascript
import { API, MongoResource } from 'arrest';const api = new API({
title: 'My API',
version: '1.0.0'
});
// Add a MongoDB-backed resource
api.addResource(new MongoResource('mongodb://localhost:27017/mydb', {
name: 'User',
collection: 'users'
}));
// Start the server
api.listen(3000);
console.log('API running at http://localhost:3000');
console.log('OpenAPI spec at http://localhost:3000/openapi.json');
`This creates a full CRUD API for users with the following endpoints:
-
GET /users - List users with filtering, sorting, pagination
- POST /users - Create a new user
- GET /users/{id} - Get user by ID
- PUT /users/{id} - Update user
- PATCH /users/{id} - Partial update user with JSON Patch
- DELETE /users/{id} - Delete user$3
Once your API is running, you can interact with it using curl or any HTTP client:
`bash
List all users
curl "http://localhost:3000/users"Create a new user
curl "http://localhost:3000/users" \
-H "Content-Type: application/json" \
-X POST \
-d '{"name": "John Doe", "email": "john@example.com"}'Get a specific user
curl "http://localhost:3000/users/507f1f77bcf86cd799439011"Update a user
curl "http://localhost:3000/users/507f1f77bcf86cd799439011" \
-H "Content-Type: application/json" \
-X PUT \
-d '{"name": "John Smith", "email": "john.smith@example.com"}'Delete a user
curl "http://localhost:3000/users/507f1f77bcf86cd799439011" -X DELETE
`$3
`javascript
import { API, Resource, Operation } from 'arrest';class CustomOperation extends Operation {
constructor(resource, path, method) {
super(resource, path, method, 'customOp');
}
getDefaultInfo() {
return {
operationId:
${this.resource.info.name}.${this.internalId},
summary: 'Custom operation',
description: 'Performs a custom operation',
responses: {
'200': {
description: 'Success',
content: {
'application/json': {
schema: { type: 'object' }
}
}
}
}
};
} async handler(req, res, next) {
try {
const result = await this.runOperation({ req, res });
res.json(result);
} catch (error) {
next(error);
}
}
async runOperation(job) {
return { message: 'Custom operation executed', timestamp: new Date() };
}
}
const api = new API();
const resource = new Resource({ name: 'Custom' });
resource.addOperation(new CustomOperation(resource, '/action', 'post'));
api.addResource(resource);
`---
Core Concepts
arrest follows a three-tier architecture:
1. API - The top-level container that manages resources and generates OpenAPI specifications
2. Resource - A collection of related operations (e.g., User resource with CRUD operations)
3. Operation - Individual HTTP endpoints that handle specific requests
$3
`javascript
import { API, Resource, Operation } from 'arrest';// 1. Create API instance
const api = new API({
title: 'My REST API',
version: '1.0.0',
description: 'A comprehensive REST API built with arrest'
});
// 2. Create resource with operations
const userResource = new Resource({
name: 'User',
path: 'users' // Optional: defaults to plural of name
});
// 3. Add custom operations to resource
userResource.addOperation('/profile', 'get', async (req, res) => {
res.json({ profile: 'user profile data' });
});
// 4. Add resource to API
api.addResource(userResource);
// 5. Start the server
api.listen(3000);
`$3
arrest automatically converts resource names to RESTful paths:
`javascript
// Resource name -> Path conversion
new Resource({ name: 'User' }); // -> /users
new Resource({ name: 'BlogPost' }); // -> /blog-posts
new Resource({ name: 'UserProfile' }); // -> /user-profiles// Custom path override
new Resource({
name: 'User',
path: 'customers', // Custom path
namePlural: 'CustomerList' // Custom plural name
});
`---
API Reference
$3
The main API container that manages resources and server configuration.
Constructor Options:
`javascript
const api = new API({
title: 'API Title',
version: '1.0.0',
description: 'API Description'
});// Additional OpenAPI document properties can be set directly:
api.document.servers = [
{ url: 'https://api.example.com', description: 'Production' },
{ url: 'http://localhost:3000', description: 'Development' }
];
api.document.security = [
{ bearerAuth: [] }
];
`Key Methods:
-
addResource(resource) - Add a resource to the API
- listen(httpPort, httpsPort?, httpsOptions?) - Start HTTP and/or HTTPS server
- router(options?) - Get Express router for integration
- attach(base, options?) - Attach to existing Express app with automatic versioning$3
Represents a collection of related operations.
Constructor Options:
`javascript
new Resource({
name: 'User', // Resource name (required)
path: 'users', // Custom path (optional)
namePlural: 'Users', // Custom plural name (optional)
description: 'User management' // OpenAPI description (optional)
})
`Key Methods:
-
addOperation(path, method, handler) - Add simple operation
- addOperation(operationInstance) - Add operation instance$3
Specialized resource for MongoDB collections with built-in CRUD operations.
Constructor:
`javascript
new MongoResource(connectionUri, options, customRoutes?)
`Options:
`javascript
{
name: 'User', // Resource name
collection: 'users', // MongoDB collection name
id: '_id', // ID field name (default: '_id')
idIsObjectId: true, // Whether ID is ObjectId (default: true)
queryLimit: 100, // Maximum query results (default: no limit)
createIndexes: false, // Auto-create indexes (default: false)
escapeProperties: false // Escape MongoDB special characters (default: false)
}
`$3
Base class for individual API operations.
Constructor:
`javascript
new Operation(resource, path, method, operationId)
`Key Methods to Override:
-
getDefaultInfo() - Return OpenAPI operation info
- handler(req, res, next) - Express request handler
- runOperation(job) - Main operation logic---
Advanced Topics
$3
arrest v14.1.0+ supports OpenAPI 3.1 with full JSON Schema Draft 2020-12 compatibility, bringing powerful new validation capabilities.
#### New Validation Keywords
1.
dependentRequired - Conditional Required FieldsSpecify that certain fields become required when another field is present:
`javascript
class CreatePaymentOperation extends Operation {
getDefaultInfo() {
return {
operationId: ${this.resource.info.name}.${this.internalId},
summary: 'Process payment',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
paymentMethod: {
type: 'string',
enum: ['credit_card', 'paypal', 'bank_transfer']
},
cardNumber: { type: 'string' },
cardExpiry: { type: 'string' },
cardCvv: { type: 'string' },
paypalEmail: { type: 'string', format: 'email' },
bankAccount: { type: 'string' }
},
required: ['paymentMethod'],
// If paymentMethod is credit_card, these fields become required
dependentRequired: {
credit_card: ['cardNumber', 'cardExpiry', 'cardCvv']
}
}
}
}
}
};
}
}
`2.
if/then/else - Conditional SchemasApply different validation rules based on field values:
`javascript
schema: {
type: 'object',
properties: {
userType: { type: 'string', enum: ['individual', 'company'] },
name: { type: 'string' },
vatNumber: { type: 'string' },
companyName: { type: 'string' }
},
required: ['userType', 'name'],
// Conditional validation based on userType
if: {
properties: { userType: { const: 'company' } }
},
then: {
required: ['vatNumber', 'companyName']
}
}
`3.
prefixItems - Tuple ValidationValidate arrays as tuples with specific types for each position:
`javascript
schema: {
type: 'object',
properties: {
location: {
type: 'array',
prefixItems: [
{ type: 'number', minimum: -90, maximum: 90 }, // latitude
{ type: 'number', minimum: -180, maximum: 180 } // longitude
],
minItems: 2,
maxItems: 2
}
}
}
`4.
const - Constant ValuesEnforce exact constant values:
`javascript
schema: {
type: 'object',
properties: {
apiVersion: { const: 'v2' },
type: { const: 'webhook' }
}
}
`5. Type Arrays for Nullable Fields
OpenAPI 3.1 uses JSON Schema's native type arrays for nullable fields:
`javascript
// OpenAPI 3.1 way (recommended)
{
type: ['string', 'null']
}// Legacy way (still supported for backward compatibility)
{
type: 'string',
nullable: true
}
`$3
arrest provides comprehensive input validation using OpenAPI v3.1 and JSON Schema Draft 2020-12:
`javascript
import { Operation } from 'arrest';class CreateUserOperation extends Operation {
constructor(resource, path, method) {
super(resource, path, method, 'createUser');
}
getDefaultInfo() {
return {
summary: 'Create a new user',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'email'],
additionalProperties: false,
properties: {
name: {
type: 'string',
minLength: 1,
maxLength: 100
},
email: {
type: 'string',
format: 'email'
},
age: {
type: 'integer',
minimum: 0,
maximum: 150
}
}
}
}
}
},
parameters: [
{
name: 'include',
in: 'query',
schema: {
type: 'array',
items: { type: 'string', enum: ['profile', 'preferences'] }
},
style: 'form',
explode: false
}
]
};
}
async runOperation(job) {
const { name, email, age } = job.req.body;
const include = job.req.query.include || [];
// Create user logic here
return { id: '123', name, email, age, created: new Date() };
}
}
`$3
arrest supports powerful querying with Resource Query Language (RQL), allowing complex filtering, sorting, and pagination through URL parameters. RQL queries are converted to MongoDB queries automatically.
#### RQL Operators Reference
Comparison Operators:
| Operator | Description | Syntax | Example | MongoDB Equivalent |
|----------|-------------|--------|---------|-------------------|
|
eq | Equal | eq(field,value) | eq(status,active) | {status: "active"} |
| ne | Not equal | ne(field,value) | ne(status,inactive) | {status: {$ne: "inactive"}} |
| lt | Less than | lt(field,value) | lt(age,30) | {age: {$lt: 30}} |
| le | Less than or equal | le(field,value) | le(price,100) | {price: {$lte: 100}} |
| gt | Greater than | gt(field,value) | gt(age,18) | {age: {$gt: 18}} |
| ge | Greater than or equal | ge(field,value) | ge(score,90) | {score: {$gte: 90}} |
| in | In array | in(field,val1,val2,...) | in(category,books,electronics) | {category: {$in: ["books","electronics"]}} |
| out | Not in array | out(field,val1,val2,...) | out(status,draft,deleted) | {status: {$nin: ["draft","deleted"]}} |
| contains | Contains value | contains(field,value) | contains(tags,urgent) | {tags: "urgent"} |
| matches | Regex match | matches(field,pattern,flags?) | matches(email,.@gmail.com,i) | {email: /.@gmail.com/i} |
| text | Full-text search | text(searchTerm,language?) | text(javascript,en) | {$text: {$search: "javascript"}} |Logical Operators:
| Operator | Description | Syntax | Example |
|----------|-------------|--------|---------|
|
and | Logical AND | and(expr1,expr2,...) | and(eq(status,active),gt(age,18)) |
| or | Logical OR | or(expr1,expr2,...) | or(eq(role,admin),eq(role,moderator)) |
| not | Logical NOT | not(expr) | not(eq(deleted,true)) |Query Modifiers:
| Operator | Description | Syntax | Example | Notes |
|----------|-------------|--------|---------|-------|
|
sort | Sort results | sort(field1,field2,...) | sort(+name,-createdAt) | Prefix with + (asc) or - (desc) |
| select | Select fields | select(field1,field2,...) | select(name,email,createdAt) | Returns only specified fields |
| limit | Pagination | limit(skip,count) | limit(0,10) | Skip N records, return M records |#### Query Parameters
In addition to RQL operators, arrest supports standard query parameters that can be used independently or combined with RQL:
| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
|
q | string | RQL query expression | ?q=eq(status,active) |
| fields | array | Comma-separated list of fields to return | ?fields=name,email,createdAt |
| sort | array | Comma-separated sort fields with +/- prefix | ?sort=+name,-createdAt |
| limit | integer | Maximum number of results (1-100) | ?limit=20 |
| skip | integer | Number of results to skip (pagination) | ?skip=40 |
| format | enum | Response format: json or csv | ?format=csv |
| csv_fields | array | Fields to include in CSV export | ?csv_fields=name,email |
| csv_names | array | Custom column names for CSV | ?csv_names=Name,Email |
| csv_options | object | CSV formatting options | ?csv_options=header=true |Combining Parameters:
`bash
Filtering + field selection + sorting + pagination
GET /users?q=eq(status,active)&fields=name,email&sort=-createdAt&limit=20&skip=0Complex RQL query with additional parameters
GET /users?q=and(gt(age,18),in(role,admin,user))&fields=name,role,age&sort=+name&limit=50CSV export with specific fields
GET /users?q=eq(status,active)&format=csv&csv_fields=name,email,createdAt&csv_options=header=true
`#### Practical Examples
Basic Filtering:
`bash
Find active users
GET /users?q=eq(status,active)Find users older than 18
GET /users?q=gt(age,18)Find users with specific roles
GET /users?q=in(role,admin,moderator,editor)Find users NOT in certain departments
GET /users?q=out(department,sales,marketing)Find products with price less than or equal to 50
GET /products?q=le(price,50)
`Logical Combinations:
`bash
Active users older than 18
GET /users?q=and(eq(status,active),gt(age,18))Users who are admins OR moderators
GET /users?q=or(eq(role,admin),eq(role,moderator))Products on sale OR cheap products
GET /products?q=or(eq(onSale,true),lt(price,20))Active users in specific countries
GET /users?q=and(eq(status,active),in(country,US,UK,CA))Complex nested conditions
GET /products?q=and(eq(available,true),or(lt(price,50),eq(category,sale)))Not deleted items
GET /items?q=not(eq(deleted,true))
`Pattern Matching:
`bash
Find emails from Gmail (case-insensitive)
GET /users?q=matches(email,.*@gmail\.com,i)Find names starting with "John"
GET /users?q=matches(name,^John,i)Case-sensitive exact pattern
GET /products?q=matches(sku,^PROD-[0-9]{4}$)
`Full-Text Search:
`bash
Search in text-indexed fields
GET /articles?q=text(javascript tutorial)Search with specific language
GET /articles?q=text(programmazione,it)
`Array Fields:
`bash
Documents containing specific tag
GET /posts?q=contains(tags,javascript)Documents with any of multiple tags
GET /posts?q=contains(tags,javascript,typescript,node)
`Sorting:
`bash
Sort by name ascending
GET /users?q=sort(+name)Sort by creation date descending
GET /users?q=sort(-createdAt)Multiple sort fields: name ascending, then age descending
GET /users?q=sort(+name,-age)Combine with filtering
GET /users?q=and(eq(status,active),sort(-createdAt))
`Field Selection (Projection):
There are two ways to select specific fields:
`bash
Using the fields query parameter (recommended for simple selections)
GET /users?fields=name,emailUsing RQL select() operator (useful in complex queries)
GET /users?q=select(name,email)Combine fields parameter with other query parameters
GET /users?fields=name,email,createdAt&sort=nameCombine RQL select() with filtering
GET /users?q=and(eq(status,active),select(name,email,role))Fields parameter with filtering (separate parameters)
GET /users?q=eq(status,active)&fields=name,email,role
`Pagination:
`bash
Get first 10 users
GET /users?q=limit(0,10)Get next 10 users (skip 10, return 10)
GET /users?q=limit(10,10)Page 3 with 20 items per page (skip 40, return 20)
GET /users?q=limit(40,20)Combine with sorting and filtering
GET /users?q=and(eq(status,active),sort(-createdAt),limit(0,20))
`#### Complete Real-World Examples
`bash
E-commerce: Active products under $100, sorted by price
GET /products?q=and(eq(status,active),lt(price,100),sort(+price))Users: Active admins or moderators, sorted by name
GET /users?q=and(eq(status,active),or(eq(role,admin),eq(role,moderator)),sort(+name))Blog: Published posts with specific tags, paginated
GET /posts?q=and(eq(published,true),in(category,tech,programming),sort(-publishedAt),limit(0,10))Search: Full-text search with filters and field selection
GET /articles?q=and(text(mongodb tutorial),ge(rating,4),select(title,author,publishedAt))Complex: Multiple conditions with nested OR
GET /orders?q=and(in(status,pending,processing),or(gt(total,100),eq(priority,high)),sort(-createdAt),limit(0,50))
`#### URL Encoding
When using RQL in URLs, special characters must be properly encoded:
`javascript
// JavaScript example: encoding RQL queries
const query = 'and(eq(status,active),gt(age,18))';
const encodedQuery = encodeURIComponent(query);
// Result: and%28eq%28status%2Cactive%29%2Cgt%28age%2C18%29%29// Using fetch
fetch(
/api/users?q=${encodeURIComponent('eq(status,active)')});// Using axios
axios.get('/api/users', {
params: {
q: 'eq(status,active)' // axios handles encoding automatically
}
});
`Common encoding requirements:
- Parentheses:
( → %28, ) → %29
- Commas: , → %2C
- Spaces: → %20 or +
- Equals: = → %3D (in values, not query params)
- Ampersands: & → %26 (in RQL expressions)`bash
Before encoding
GET /users?q=and(eq(status,active),matches(name,John.*))After encoding (what actually gets sent)
GET /users?q=and%28eq%28status%2Cactive%29%2Cmatches%28name%2CJohn.*%29%29
`#### Best Practices and Tips
1. Use appropriate operators: Choose the most specific operator for your needs
- Use
in instead of multiple or(eq(...)) expressions
- Use ge/le for inclusive ranges, gt/lt for exclusive2. Optimize complex queries:
- Put most selective filters first in
and() expressions
- Use indexes on frequently queried fields
- Consider using select() to reduce payload size3. Text search requires indexes:
`javascript
// Define text index in your MongoResource
getIndexes() {
return [
{ key: { title: 'text', description: 'text' } }
];
}
`4. ObjectId handling:
- RQL automatically converts string IDs to MongoDB ObjectId for
_id field
- Example: eq(_id,507f1f77bcf86cd799439011) works automatically5. Regex performance:
- Avoid leading wildcards when possible:
^John is faster than .*John
- Use case-insensitive flag sparingly
- Consider full-text search for complex text queries6. Combining operators:
`bash
# Good: Logical structure
GET /items?q=and(eq(active,true),or(lt(price,50),eq(sale,true)),sort(-date),limit(0,20)) # Avoid: Overly complex nesting - split into multiple requests if needed
`7. Default query limits:
- Consider setting
queryLimit in MongoResource options to prevent excessive results
- Always use limit() for paginated interfaces8. Testing RQL queries:
`bash
# Use curl for testing
curl "http://localhost:3000/users?q=$(node -p 'encodeURIComponent("eq(status,active)")')" # Or use a tool that handles encoding
http GET "localhost:3000/users" q=="eq(status,active)" # HTTPie
`$3
#### OAuth2 Scopes
`javascript
import { API, MongoResource } from 'arrest';class SecureAPI extends API {
initSecurity(req, res, next) {
// Extract and validate OAuth2 token
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Missing authorization token' });
}
// Validate token and set scopes
req.scopes = ['read:users', 'write:users']; // From token validation
next();
}
}
class SecureOperation extends Operation {
get swaggerScopes() {
return {
'oauth2': ['read:users', 'write:users']
};
}
}
`#### CASL Ability-based Permissions
`javascript
import { defineAbility } from '@casl/ability';class AuthorizedAPI extends API {
initSecurity(req, res, next) {
// Define user abilities based on their role
req.ability = defineAbility((can, cannot) => {
if (req.user.role === 'admin') {
can('manage', 'all');
} else if (req.user.role === 'user') {
can('read', 'User', { ownerId: req.user.id });
can('update', 'User', { ownerId: req.user.id });
cannot('delete', 'User');
}
});
next();
}
}
`$3
#### Index Management
arrest provides automatic MongoDB index management with verification and optional creation of missing indexes. This ensures your collections have the required indexes for optimal query performance.
How Index Management Works:
1. Automatic Verification: When a collection is first accessed, arrest checks if the defined indexes exist
2. Optional Auto-Creation: If
createIndexes: true, missing indexes are automatically created
3. Warning System: If createIndexes: false, missing indexes trigger warnings in the logs
4. One-Time Check: Index verification happens only once per application instanceDefining Indexes:
Override the
getIndexes() method to define custom indexes:`javascript
import { MongoResource } from 'arrest';class UserResource extends MongoResource {
constructor() {
super('mongodb://localhost:27017/myapp', {
name: 'User',
collection: 'users',
createIndexes: true // Enable automatic index creation
});
}
getIndexes() {
return [
// Keep the default ID index (if using custom ID field)
...super.getIndexes(),
// Unique email index
{
key: { email: 1 },
unique: true
},
// Compound index for queries
{
key: { status: 1, createdAt: -1 }
},
// Sparse index (only indexes documents with the field)
{
key: { phoneNumber: 1 },
unique: true,
sparse: true
},
// TTL index (expires documents after time)
{
key: { sessionExpiry: 1 },
expireAfterSeconds: 3600 // 1 hour
},
// Text index for full-text search
{
key: { title: 'text', description: 'text' }
},
// Nested field index
{
key: { 'profile.tags': 1 }
}
];
}
}
`Default Index Behavior:
By default, if you specify a custom ID field (other than
_id), arrest automatically creates a unique index on that field:`javascript
new MongoResource('mongodb://localhost:27017/myapp', {
name: 'User',
collection: 'users',
id: 'userId' // Custom ID field - automatically gets unique index
});
`Index Properties:
arrest supports the following MongoDB index properties:
| Property | Type | Description | Example |
|----------|------|-------------|---------|
|
key | object | Fields to index (required) | { email: 1, status: -1 } |
| unique | boolean | Enforce uniqueness | true |
| sparse | boolean | Only index documents with field | true |
| min | number | Minimum value for numeric index | 0 |
| max | number | Maximum value for numeric index | 100 |
| expireAfterSeconds | number | TTL for document expiration | 86400 (1 day) |Index Verification Process:
When
createIndexes: false, arrest logs warnings for missing indexes:`
[WARN] User: missing index { key: { email: 1 }, unique: true }
`When
createIndexes: true, arrest creates missing indexes automatically:`
[INFO] User: creating missing index { key: { email: 1 }, unique: true }
`#### Advanced MongoDB Operations
`javascript
import { MongoResource, QueryMongoOperation } from 'arrest';class AdvancedUserResource extends MongoResource {
constructor() {
super('mongodb://localhost:27017/myapp', {
name: 'User',
collection: 'users',
createIndexes: true
});
}
getIndexes() {
return [
{ key: { email: 1 }, unique: true },
{ key: { 'profile.tags': 1 } },
{ key: { createdAt: -1 } }
];
}
}
// Custom aggregation operation
class UserStatsOperation extends QueryMongoOperation {
async prepareQuery(job) {
// Return MongoDB aggregation pipeline instead of simple query
return [
{ $match: { status: 'active' } },
{ $group: {
_id: '$department',
count: { $sum: 1 },
averageAge: { $avg: '$age' }
}
},
{ $sort: { count: -1 } }
];
}
}
`$3
arrest provides built-in CSV export functionality for any resource. Simply add
format=csv to your query parameters to export data as CSV instead of JSON.#### Basic CSV Export
`bash
Export all users as CSV
GET /users?format=csvExport with specific fields
GET /users?format=csv&csv_fields=name,email,createdAtExport with custom column names
GET /users?format=csv&csv_fields=name,email&csv_names=Full Name,Email Address
`#### CSV Query Parameters
| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
|
format | string | Set to csv to enable CSV export | format=csv |
| csv_fields | array | Comma-separated list of fields to include | csv_fields=name,email,age |
| csv_names | array | Custom column names (must match csv_fields count) | csv_names=Name,Email,Age |
| csv_options | object | CSV formatting options | csv_options=header=true |#### CSV Options
The
csv_options parameter supports these formatting options:-
header=true/false - Include header row (default: true)
- delimiter=, - Field delimiter (default: comma)
- quote=" - Quote character for values with special chars
- escape=\ - Escape character
- linebreak=\n - Line break character#### Combining CSV with RQL
CSV export works seamlessly with RQL queries and other parameters:
`bash
Export active users only
GET /users?q=eq(status,active)&format=csv&csv_fields=name,emailExport with sorting and pagination
GET /users?q=and(eq(status,active),sort(-createdAt),limit(0,100))&format=csvComplex query with field selection
GET /orders?q=and(gt(total,100),in(status,pending,processing))&format=csv&csv_fields=id,customer,total,status&csv_names=Order ID,Customer Name,Total Amount,StatusCSV with nested fields
GET /users?format=csv&csv_fields=name,email,profile.city,profile.country&csv_names=Name,Email,City,Country
`#### Programmatic CSV Export
Use CSV export in your custom operations:
`javascript
import { QueryMongoOperation } from 'arrest';class CustomReportOperation extends QueryMongoOperation {
getDefaultInfo() {
return {
operationId:
${this.resource.info.name}.${this.internalId},
summary: 'Generate custom report',
parameters: [
{
name: 'format',
in: 'query',
schema: { type: 'string', enum: ['json', 'csv'] }
},
{
name: 'csv_fields',
in: 'query',
schema: {
type: 'array',
items: { type: 'string' }
},
style: 'form',
explode: false
}
],
responses: {
'200': {
description: 'Report data',
content: {
'application/json': { schema: { type: 'array' } },
'text/csv': { schema: { type: 'string' } }
}
}
}
};
}
}
`$3
arrest supports dual REST and JSON-RPC interfaces:
`javascript
import { RPCOperation } from 'arrest';class UserRPCOperation extends RPCOperation {
async getUserProfile(params) {
const { userId } = params;
// Fetch user profile logic
return { profile: { id: userId, name: 'John Doe' } };
}
async updateUserProfile(params) {
const { userId, updates } = params;
// Update logic
return { success: true, updated: updates };
}
}
// JSON-RPC calls:
// POST /users/rpc
// {"jsonrpc": "2.0", "method": "getUserProfile", "params": {"userId": "123"}, "id": 1}
`$3
Complex data processing with pipeline support:
`javascript
import { PipelineOperation } from 'arrest';class DataProcessingPipeline extends PipelineOperation {
async runOperation(job) {
let data = await super.runOperation(job);
// Apply transformations
data = this.filterSensitiveData(data);
data = this.calculateDerivedFields(data);
data = this.formatForOutput(data, job.req.query.format);
return data;
}
filterSensitiveData(data) {
// Remove sensitive fields based on user permissions
return data.map(item => this.filterFields(item, job.req.ability));
}
}
`$3
arrest works seamlessly with existing Express applications:
`javascript
import express from 'express';
import { API, MongoResource } from 'arrest';const app = express();
const api = new API();
// Add resources to API
api.addResource(new MongoResource('mongodb://localhost:27017/mydb', {
name: 'User'
}));
// Mount API on Express app
app.use('/api/v1', await api.router());
// Add other Express routes
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
app.listen(3000);
`$3
arrest provides a comprehensive and flexible error handling system with multiple approaches to handle different types of errors. The framework uses the
RESTError class as the foundation for error management, along with built-in middleware for validation and generic errors.#### Error Handling Methodologies
1. RESTError Class - The Foundation
The
RESTError class is the core of arrest's error handling system:`javascript
import { API } from 'arrest';// Create and throw a RESTError
throw API.newError(404, 'User not found', { userId: '123' });
// Or use the fireError method (throws immediately)
API.fireError(403, 'Insufficient privileges', { required: 'admin' });
// Custom error with original error preserved
try {
await someOperation();
} catch (originalError) {
throw API.newError(500, 'Operation failed', { details: 'Database error' }, originalError);
}
`2. Operation-Level Error Handling
Handle errors at the operation level for fine-grained control:
`javascript
import { Operation } from 'arrest';class CustomOperation extends Operation {
async handler(req, res, next) {
try {
const result = await this.runOperation({ req, res });
res.json(result);
} catch (error) {
// Transform and forward error
if (error.name === 'MongoError' && error.code === 11000) {
next(API.newError(409, 'Duplicate entry', { field: 'email' }));
} else {
next(error);
}
}
}
async runOperation(job) {
// Your operation logic here
// Errors thrown here will be caught by handler
if (!job.req.body.email) {
API.fireError(400, 'Email is required');
}
return { success: true };
}
// Override errorHandler for operation-specific error handling
errorHandler(err, req, res, next) {
req.logger.error('Operation error', err);
// Add custom logging or transformation
next(err); // Forward to API-level handler
}
}
`3. API-Level Global Error Handler
Customize the global error handler by overriding the
handleError method:`javascript
import { API, RESTError } from 'arrest';
import { ValidationError } from 'openapi-police';class CustomAPI extends API {
handleError(err, req, res, next) {
// Handle RESTError
if (err.name === 'RESTError') {
req.logger.error('REST ERROR', err);
return RESTError.send(res, err.code, err.message, err.info);
}
// Handle OpenAPI validation errors
if (err.name === 'ValidationError') {
req.logger.error('VALIDATION ERROR', err);
return RESTError.send(res, 400, 'Validation failed', ValidationError.getInfo(err));
}
// Handle MongoDB errors
if (err.name === 'MongoError') {
if (err.code === 11000) {
req.logger.warn('Duplicate key error', err);
return RESTError.send(res, 409, 'Resource already exists', {
field: Object.keys(err.keyPattern || {})[0]
});
}
req.logger.error('MongoDB error', err);
return RESTError.send(res, 500, 'Database error');
}
// Handle custom application errors
if (err.type === 'BusinessRuleViolation') {
req.logger.warn('Business rule violation', err);
return RESTError.send(res, 422, err.message, err.details);
}
// Default error handling
req.logger.error('GENERIC ERROR', err, err.stack);
RESTError.send(res, 500, 'Internal server error');
}
}
`4. Validation Error Handling
arrest automatically validates requests against OpenAPI schemas using openapi-police. Handle validation errors:
`javascript
import { ValidationError } from 'openapi-police';class ValidationAwareAPI extends API {
handleError(err, req, res, next) {
if (err.name === 'ValidationError') {
const info = ValidationError.getInfo(err);
// Return detailed validation info
return res.status(400).json({
error: 'Validation failed',
message: err.message,
path: err.path,
details: info,
timestamp: new Date().toISOString()
});
}
super.handleError(err, req, res, next);
}
}
`Example Validation Error Response:
When a request fails validation, openapi-police returns detailed error information:
`json
{
"error": 400,
"message": "ValidationError",
"info": {
"failedValidation": true,
"path": "body",
"errors": [
{
"keyword": "required",
"dataPath": "",
"schemaPath": "#/required",
"params": { "missingProperty": "email" },
"message": "must have required property 'email'"
},
{
"keyword": "format",
"dataPath": ".email",
"schemaPath": "#/properties/email/format",
"params": { "format": "email" },
"message": "must match format \"email\""
},
{
"keyword": "minimum",
"dataPath": ".age",
"schemaPath": "#/properties/age/minimum",
"params": { "comparison": ">=", "limit": 0 },
"message": "must be >= 0"
}
]
}
}
`Common Validation Error Types:
| Keyword | Description | Example Message |
|---------|-------------|-----------------|
|
required | Required field is missing | "must have required property 'email'" |
| type | Wrong data type | "must be string" |
| format | Invalid format (email, date, etc.) | "must match format \"email\"" |
| minimum/maximum | Number out of range | "must be >= 0" |
| minLength/maxLength | String length violation | "must NOT be shorter than 1 characters" |
| pattern | Regex pattern mismatch | "must match pattern \"^[A-Z]\"" |
| enum | Value not in allowed list | "must be equal to one of the allowed values" |
| additionalProperties | Extra properties not allowed | "must NOT have additional properties" |5. Security and Authorization Errors
Handle authentication and authorization errors with proper status codes:
`javascript
class SecureAPI extends API {
initSecurity(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) {
return next(API.newError(401, 'Authentication required', {
hint: 'Include Authorization header with Bearer token'
}));
}
try {
const user = this.validateToken(token);
req.user = user;
req.scopes = user.scopes;
next();
} catch (error) {
next(API.newError(401, 'Invalid or expired token'));
}
}
handleError(err, req, res, next) {
// Handle authorization errors with detailed info
if (err.code === 403) {
return res.status(403).json({
error: 'Forbidden',
message: err.message || 'Insufficient privileges',
required: err.info?.required,
available: err.info?.available
});
}
if (err.code === 401) {
return res.status(401).json({
error: 'Unauthorized',
message: err.message || 'Authentication required',
hint: err.info?.hint
});
}
super.handleError(err, req, res, next);
}
}
`6. Async Error Handling in Operations
All operation methods support async/await and automatically catch errors:
`javascript
import { MongoResource, QueryMongoOperation } from 'arrest';class SafeQueryOperation extends QueryMongoOperation {
async runOperation(job) {
try {
const result = await super.runOperation(job);
// Business logic validation
if (result.length === 0) {
API.fireError(404, 'No matching records found');
}
return result;
} catch (error) {
// Add context to the error
throw API.newError(
error.code || 500,
'Query execution failed',
{
query: job.req.query,
collection: this.resource.info.collection
},
error
);
}
}
}
`7. 404 Not Found Handling
arrest provides built-in 404 handling. Customize it if needed:
`javascript
const api = new API();// Use the built-in 404 handler (default)
// This is already added by api.listen()
app.use(API.handle404Error);
// Or create a custom 404 handler
app.use((req, res, next) => {
req.logger.warn('404 - Resource not found', {
path: req.path,
method: req.method
});
res.status(404).json({
error: 'Not Found',
message: 'The requested resource was not found',
path: req.path,
suggestion: 'Check your URL and HTTP method',
availableEndpoints: '/openapi.json'
});
});
`8. Error Logging and Monitoring
arrest uses the
debuggo logger. Leverage it for error tracking:`javascript
class MonitoredAPI extends API {
handleError(err, req, res, next) {
// Log with context
req.logger.error('Error occurred', {
error: err.message,
stack: err.stack,
code: err.code,
user: req.user?.id,
path: req.path,
method: req.method,
timestamp: new Date()
}); // Send to external monitoring (e.g., Sentry, DataDog)
if (err.code >= 500) {
this.sendToMonitoring({
level: 'error',
error: err,
request: {
path: req.path,
method: req.method,
headers: req.headers,
body: req.body
}
});
}
super.handleError(err, req, res, next);
}
sendToMonitoring(data) {
// Integration with your monitoring service
// e.g., Sentry.captureException(data.error)
}
}
`9. Structured Error Responses
Create consistent error response formats:
`javascript
class StructuredErrorAPI extends API {
handleError(err, req, res, next) {
const errorResponse = {
status: 'error',
timestamp: new Date().toISOString(),
path: req.path,
method: req.method,
error: {
code: err.code || 500,
name: err.name,
message: err.message || 'An error occurred',
details: err.info,
},
requestId: req.id
}; // Add stack trace in development only
if (process.env.NODE_ENV === 'development' && err.stack) {
errorResponse.error.stack = err.stack;
}
// Don't leak internal details in production
if (process.env.NODE_ENV === 'production' && errorResponse.error.code >= 500) {
errorResponse.error.message = 'Internal server error';
delete errorResponse.error.details;
}
req.logger.error('Error response', errorResponse);
res.status(errorResponse.error.code).json(errorResponse);
}
}
`10. Custom Error Types
Create domain-specific error types:
`javascript
class BusinessError extends Error {
constructor(message, code = 422, details = {}) {
super(message);
this.name = 'BusinessError';
this.code = code;
this.details = details;
}
}class UserService {
async createUser(userData) {
if (!this.isValidEmail(userData.email)) {
throw new BusinessError('Invalid email format', 400, {
field: 'email',
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$'
});
}
if (await this.emailExists(userData.email)) {
throw new BusinessError('Email already registered', 409, {
field: 'email',
value: userData.email
});
}
// Create user...
}
}
class BusinessAwareAPI extends API {
handleError(err, req, res, next) {
if (err.name === 'BusinessError') {
req.logger.warn('Business error', err);
return res.status(err.code).json({
error: err.name,
message: err.message,
details: err.details
});
}
super.handleError(err, req, res, next);
}
}
`#### Error Response Format
All errors follow a consistent JSON format:
`json
{
"error": 400,
"message": "Validation failed",
"info": {
"path": "body.email",
"errors": ["must be a valid email address"]
}
}
`#### Common HTTP Error Codes
| Code | Meaning | When to Use |
|------|---------|-------------|
| 400 | Bad Request | Validation errors, malformed requests |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Valid authentication but insufficient privileges |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate resource (e.g., unique constraint violation) |
| 422 | Unprocessable Entity | Business rule violations |
| 429 | Too Many Requests | Rate limiting |
| 500 | Internal Server Error | Unexpected server errors |
| 503 | Service Unavailable | Temporary unavailability (database down, etc.) |
#### Best Practices
1. Always use RESTError for HTTP errors: Use
API.newError() or API.fireError() for consistency
2. Preserve original errors: Pass the original error as the 4th parameter to maintain stack traces
3. Log appropriately: Use different log levels (error, warn, info) based on severity
4. Don't leak sensitive info: In production, sanitize error messages and stack traces
5. Use specific HTTP codes: Choose the most appropriate status code for each error type
6. Provide actionable information: Include hints or suggestions in error responses when helpful
7. Handle async errors: Always use try/catch or .catch() with async operations
8. Test error paths: Write tests for both success and error scenarios
9. Monitor 5xx errors: Set up alerts for server errors in production
10. Document errors: Include error responses in OpenAPI specifications---
Development & Production
$3
Full TypeScript definitions are included with proper OpenAPI v3.1 types:
`typescript
import { API, MongoResource, Operation } from 'arrest';
import type { Request, Response } from 'express';
import type { OpenAPIV3_1 } from 'openapi-police';interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
interface CreateUserRequest {
name: string;
email: string;
age?: number;
}
class TypedUserOperation extends Operation {
getDefaultInfo(): OpenAPIV3_1.OperationObject {
return {
operationId:
${this.resource.info.name}.${this.internalId},
summary: 'Create a new user',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 }
},
additionalProperties: false
}
}
}
},
responses: {
'201': {
description: 'User created',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
}
}
}
}
}
}
};
} async runOperation(job: { req: Request; res: Response }): Promise {
const userData = job.req.body as CreateUserRequest;
// Type-safe implementation
const user: User = {
id: 'generated-id',
name: userData.name,
email: userData.email,
createdAt: new Date()
};
return user;
}
async handler(req: Request, res: Response, next: Function): Promise {
try {
const result = await this.runOperation({ req, res });
res.status(201).json(result);
} catch (error) {
next(error);
}
}
}
// Usage with typed API
const api = new API({
title: 'Typed API',
version: '1.0.0'
});
// TypeScript will provide full IntelliSense for all methods
const userResource = new MongoResource('mongodb://localhost:27017/mydb', {
name: 'User',
collection: 'users'
});
api.addResource(userResource);
`$3
Optimization Tips:
1. Use MongoDB indexes - Define indexes for frequently queried fields
2. Implement caching - Use Redis or memory caching for frequently accessed data
3. Limit query results - Set reasonable queryLimit on resources
4. Use projections - Only fetch needed fields with the
fields parameter
5. Enable compression - Use gzip compression in productionProduction Configuration:
`javascript
import { API, MongoResource } from 'arrest';const api = new API({
title: 'Production API',
version: '1.0.0'
});
// Production MongoDB resource with optimization
api.addResource(new MongoResource('mongodb://mongo-cluster/prod-db', {
name: 'User',
collection: 'users',
queryLimit: 100, // Limit results
createIndexes: true, // Auto-create indexes
escapeProperties: true // Security: escape special chars
}));
// Start with both HTTP and HTTPS
api.listen(8080, 8443, {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem')
});
`---
Contributing & Resources
$3
This project uses pnpm as the package manager and TypeScript for development.
Available Scripts:
`bash
Install dependencies
pnpm installBuild the project (compile TypeScript)
pnpm run buildRun tests (builds and compiles tests first)
pnpm run testRun tests with coverage
pnpm run coverCheck that coverage meets 100% requirement
pnpm run check-coverageClean build artifacts
pnpm run cleanWatch mode for tests
pnpm run test:watch
`$3
- Source:
src/ (TypeScript)
- Compiled: dist/ (JavaScript + type definitions)
- Tests: test/ts/ (TypeScript) → test/ (compiled JavaScript)---
$3
Contributions are welcome! Please follow these guidelines:
1. Fork the repository on GitHub
2. Create a feature branch (
git checkout -b feature/my-feature)
3. Write tests for your changes
4. Ensure 100% test coverage is maintained
5. Run the test suite and verify all tests pass:`bash
pnpm install
pnpm run build
pnpm run test
pnpm run check-coverage # Must show 100% coverage
``6. Commit your changes using conventional commits
7. Push to your fork and submit a pull request
Testing Requirements:
- All code must have 100% test coverage (statements, branches, functions, lines)
- Tests use mocha as the test framework
- MongoDB integration tests use mongodoki for test database setup
- Tests are written in TypeScript and compiled before running
---
MIT License - see the LICENSE file for details.
---
- jsonref - JSON Reference resolution
- openapi-police - OpenAPI validation utilities