Core Extra Plugins
This guide covers the additional plugins that extend the core functionality of json-rest-api with advanced features for production applications.
Plugin: audit-log
Track all changes to your data for security, compliance, and debugging.
Introduction
The Audit Log plugin records security-relevant events like authentication attempts, data modifications, and access violations. It’s essential for compliance requirements (GDPR, HIPAA) and security monitoring.
What json-rest-api provides
- Automatic logging of CRUD operations
- Security event tracking (auth failures, rate limiting)
- Multiple log destinations (console, file, database, remote)
- Configurable log formats (JSON, syslog, CEF)
- Request context capture (IP, user agent, request ID)
- In-memory storage with size limits
- Async storage support
How to use it
import { AuditLogPlugin } from 'json-rest-api';
api.use(AuditLogPlugin, {
// Log destinations
logToConsole: true,
logToDatabase: true,
// What to log
logAuthFailures: true,
logAuthSuccess: false,
logDataModification: true,
logDataAccess: false, // Be careful - can be verbose
// Storage (use Redis/DB in production)
storage: new Map(),
maxLogSize: 10000,
// Format
format: 'json',
includeIp: true,
includeUserAgent: true,
// Custom handler
onSecurityEvent: async (event) => {
if (event.severity === 'CRITICAL') {
await sendAlertToOps(event);
}
}
});
What to expect
// Authentication failure logged automatically
POST /api/login
{
"email": "user@example.com",
"password": "wrong"
}
// Logs: {
// type: 'AUTH_FAILURE',
// severity: 'WARNING',
// user: 'user@example.com',
// ip: '192.168.1.1',
// timestamp: '2024-01-20T10:30:00Z'
// }
// Data modification logged
PUT /api/users/123
{
"role": "admin"
}
// Logs: {
// type: 'DATA_MODIFICATION',
// resource: 'users',
// action: 'update',
// resourceId: '123',
// changes: { role: 'admin' },
// userId: '456',
// timestamp: '2024-01-20T10:31:00Z'
// }
// Query audit logs
const logs = await api.getAuditLogs({
type: 'DATA_MODIFICATION',
resource: 'users',
startDate: '2024-01-20',
endDate: '2024-01-21'
});
Limitations
- In-memory storage is limited by maxLogSize
- File logging requires write permissions
- High-volume logging can impact performance
- Sensitive data in logs needs careful handling
Plugin: authorization
Role-based access control (RBAC) with fine-grained permissions.
Introduction
The Authorization plugin provides comprehensive access control with roles, permissions, and ownership-based rules. It integrates seamlessly with field-level permissions in schemas.
What json-rest-api provides
- Role definitions with permissions
- Resource-level access rules
- Ownership-based permissions (.own suffix)
- User enhancement bridge pattern
- Automatic permission checking in hooks
- Public/authenticated/owner access levels
- Permission inheritance and wildcards
- Conditional access rules
How to use it
import { AuthorizationPlugin } from 'json-rest-api';
api.use(AuthorizationPlugin, {
requireAuth: true, // Require auth by default
superAdminRole: 'admin',
ownerField: 'userId', // Default owner field
// Bridge to your auth system
enhanceUser: async (basicUser) => {
// Add roles from your auth system
const dbUser = await getUserFromDatabase(basicUser.id);
return {
...basicUser,
roles: dbUser.roles,
permissions: dbUser.permissions
};
}
});
// Define roles
api.defineRole('admin', {
permissions: ['*'], // All permissions
description: 'Full system access'
});
api.defineRole('editor', {
permissions: [
'posts.read',
'posts.create',
'posts.update.own', // Only own posts
'posts.delete.own',
'comments.*'
]
});
api.defineRole('user', {
permissions: [
'posts.read',
'posts.create',
'comments.create',
'profile.update.own'
]
});
// Configure resource auth
api.configureResourceAuth('posts', {
ownerField: 'authorId',
public: ['read'], // Anyone can read
authenticated: ['create'], // Logged in users can create
owner: ['update', 'delete'], // Only owner can modify
permissions: {
publish: 'posts.publish',
feature: 'posts.feature'
}
});
// Use in routes/hooks
api.hook('beforeUpdate', async (context) => {
if (context.options.type === 'posts') {
const post = await api.resources.posts.get(context.id);
// Check custom permission
if (post.data.attributes.status === 'published') {
const canPublish = await api.checkPermission(
context.options.user,
'posts.publish'
);
if (!canPublish) {
throw new ForbiddenError('Cannot modify published posts');
}
}
}
});
What to expect
// User with 'editor' role
const user = {
id: '123',
roles: ['editor']
};
// Can create posts
await api.resources.posts.create({
title: 'My Post',
content: '...'
}, { user }); // Success
// Can update own post
await api.resources.posts.update('456', {
title: 'Updated'
}, { user }); // Success if authorId matches user.id
// Cannot update others' posts
await api.resources.posts.update('789', {
title: 'Hacked!'
}, { user }); // Throws ForbiddenError
// Admin can do anything
const admin = { id: '999', roles: ['admin'] };
await api.resources.posts.delete('789', { admin }); // Success
Limitations
- Requires user object in context
- Permission checks add overhead
- Complex permission rules can be hard to debug
- No built-in UI for role management
Plugin: computed
Create virtual resources that generate data on-the-fly without database storage.
Introduction
The Computed plugin enables you to create API resources that calculate data dynamically. Perfect for aggregations, external API proxies, real-time calculations, and mock data.
What json-rest-api provides
- No database required - data generated on demand
- Full API feature support (validation, auth, filtering, sorting)
- Access to other resources (computed or database)
- Optional CRUD operation support
- Automatic filtering/sorting/pagination
- Context access for complex calculations
How to use it
import { ComputedPlugin } from 'json-rest-api';
api.use(ComputedPlugin);
// Simple computed resource
api.addResource('server-stats', statsSchema, {
compute: {
get: async (id) => {
if (id !== 'current') {
throw new NotFoundError('Only "current" stats available');
}
return {
id: 'current',
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
cpuUsage: process.cpuUsage(),
timestamp: new Date().toISOString()
};
}
}
});
// Aggregating data from other resources
api.addResource('user-statistics', userStatsSchema, {
compute: {
get: async (userId, context) => {
const user = await context.api.resources.users.get(userId);
const posts = await context.api.resources.posts.query({
filter: { authorId: userId }
});
const comments = await context.api.resources.comments.query({
filter: { userId }
});
return {
id: userId,
username: user.data.attributes.name,
postCount: posts.meta.total,
commentCount: comments.meta.total,
totalLikes: posts.data.reduce((sum, p) =>
sum + p.attributes.likes, 0),
joinDate: user.data.attributes.createdAt,
lastActive: posts.data[0]?.attributes.createdAt ||
user.data.attributes.createdAt
};
},
query: async (params, context) => {
// Get all users and compute stats for each
const users = await context.api.resources.users.query({});
const stats = await Promise.all(
users.data.map(async (user) => {
const posts = await context.api.resources.posts.query({
filter: { authorId: user.id }
});
return {
id: user.id,
username: user.attributes.name,
postCount: posts.meta.total
};
})
);
// Plugin handles filtering/sorting/pagination!
return stats;
}
}
});
// External API proxy
api.addResource('weather', weatherSchema, {
compute: {
get: async (city) => {
const response = await fetch(
`https://api.weather.com/current?city=${city}`
);
const data = await response.json();
return {
id: city,
temperature: data.temp,
conditions: data.conditions,
humidity: data.humidity,
lastUpdated: new Date().toISOString()
};
}
}
});
What to expect
// Get server stats
GET /api/server-stats/current
// Returns: {
// "data": {
// "id": "current",
// "type": "server-stats",
// "attributes": {
// "uptime": 3600,
// "memoryUsage": {...},
// "cpuUsage": {...},
// "timestamp": "2024-01-20T10:00:00Z"
// }
// }
// }
// Get user statistics with caching
GET /api/user-statistics/123
// Computes and returns aggregated data
// Query with filtering and sorting (handled automatically!)
GET /api/user-statistics?filter[postCount][gte]=10&sort=-postCount
// Plugin filters and sorts the computed results
Limitations
- No persistence - data regenerated each request
- Can be slow for complex calculations
- No real-time updates without polling
- Memory constraints for large datasets
Plugin: cors
Zero-config CORS with automatic platform detection.
Introduction
The CORS plugin provides hassle-free Cross-Origin Resource Sharing configuration. It automatically detects your deployment platform and configures appropriate CORS headers.
What json-rest-api provides
- Auto-detection for Vercel, Netlify, Heroku, AWS, etc.
- Development-friendly localhost defaults
- Environment variable configuration
- Dynamic origin validation
- Preflight request handling
- Credentials support
- Custom header configuration
How to use it
import { CorsPlugin } from 'json-rest-api';
// Zero config - just works!
api.use(CorsPlugin);
// Or customize
api.use(CorsPlugin, {
// Allowed origins (auto-detected if not set)
origins: [
'https://myapp.com',
'https://staging.myapp.com'
],
// Or use a function
origin: (origin) => {
return origin.endsWith('.myapp.com');
},
// Other options
credentials: true,
maxAge: 86400, // Preflight cache for 24 hours
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'],
// Development mode
development: {
origins: ['http://localhost:3000', 'http://localhost:5173']
}
});
What to expect
Platform detection examples:
// Vercel deployment
process.env.VERCEL = '1'
process.env.VERCEL_URL = 'my-app-git-abc123.vercel.app'
// Automatically allows: https://my-app-git-abc123.vercel.app
// Netlify deployment
process.env.NETLIFY = 'true'
process.env.URL = 'https://my-app.netlify.app'
// Automatically allows: https://my-app.netlify.app
// Development
process.env.NODE_ENV = 'development'
// Automatically allows: http://localhost:*
// Manual configuration via environment
process.env.CORS_ORIGINS = 'https://app1.com,https://app2.com'
// Allows both origins
Response headers:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Limitations
- Platform detection requires specific environment variables
- Dynamic origins can be slower than static lists
- Wildcard origins reduce security
- Some browsers have CORS bugs
Plugin: csrf
CSRF protection for state-changing operations.
Introduction
The CSRF plugin protects against Cross-Site Request Forgery attacks by requiring a valid token for state-changing operations (POST, PUT, DELETE).
What json-rest-api provides
- Automatic token generation
- Double-submit cookie pattern
- Synchronizer token pattern support
- Safe method exemptions
- Custom header support
- Development mode bypass
- Token rotation
How to use it
import { CsrfPlugin } from 'json-rest-api';
api.use(CsrfPlugin, {
// Token settings
tokenLength: 32,
tokenName: 'csrf-token',
headerName: 'X-CSRF-Token',
cookieName: '_csrf',
// Cookie options
cookie: {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 86400000 // 24 hours
},
// Exemptions
safeMethods: ['GET', 'HEAD', 'OPTIONS'],
skipPaths: ['/api/webhooks'], // External webhooks
// Development
development: {
enabled: false // Disable in development
}
});
// In your frontend
async function makeRequest() {
// Get token from cookie or meta tag
const token = document.querySelector('meta[name="csrf-token"]').content;
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ title: 'New Post' })
});
}
What to expect
// GET request - token returned in response
GET /api/posts
// Response headers:
// Set-Cookie: _csrf=abc123...; HttpOnly; Secure; SameSite=Strict
// Response body includes token
// POST without token - rejected
POST /api/posts
Content-Type: application/json
{
"title": "New Post"
}
// Response: 403 Forbidden
// {
// "error": "Invalid CSRF token"
// }
// POST with valid token - accepted
POST /api/posts
Content-Type: application/json
X-CSRF-Token: abc123...
{
"title": "New Post"
}
// Response: 201 Created
Limitations
- Requires cookie support
- Not suitable for pure API services
- Token rotation can cause issues with concurrent requests
- Mobile apps need special handling
Plugin: jwt
JSON Web Token authentication with refresh token support.
Introduction
The JWT plugin provides secure token-based authentication using industry-standard JSON Web Tokens. It replaces insecure session tokens with cryptographically signed JWTs.
What json-rest-api provides
- Secure token generation and verification
- Refresh token support with rotation
- Multiple signing algorithms (HS256, RS256)
- Token expiration and renewal
- Timing attack prevention
- Legacy token migration support
- Blacklist/revocation support
How to use it
import { JwtPlugin } from 'json-rest-api';
api.use(JwtPlugin, {
// Signing configuration
secret: process.env.JWT_SECRET, // For HS256
// OR for RS256:
// privateKey: fs.readFileSync('private.key'),
// publicKey: fs.readFileSync('public.key'),
// Token options
expiresIn: '15m', // Access token expiry
refreshExpiresIn: '30d', // Refresh token expiry
issuer: 'my-api',
audience: 'my-app',
// Storage for refresh tokens (use Redis in production)
tokenStore: new Map(),
// Hooks
beforeSign: async (payload) => {
// Add custom claims
payload.permissions = await getUserPermissions(payload.sub);
return payload;
},
afterVerify: async (payload) => {
// Load full user object
return await getUserById(payload.sub);
}
});
// Generate tokens
const tokens = await api.generateToken({
sub: user.id, // Subject (user ID)
email: user.email,
roles: user.roles
});
// Returns: {
// accessToken: 'eyJhbGc...',
// refreshToken: 'f47ac10b...',
// expiresIn: 900,
// tokenType: 'Bearer'
// }
// Verify token
try {
const payload = await api.verifyToken(token);
console.log('User ID:', payload.sub);
} catch (error) {
console.error('Invalid token:', error.message);
}
// Refresh tokens
const newTokens = await api.refreshToken(refreshToken);
// Revoke tokens
await api.revokeToken(refreshToken);
await api.revokeAllUserTokens(userId);
What to expect
// Login endpoint
POST /api/login
{
"email": "user@example.com",
"password": "secret"
}
// Response:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"expiresIn": 900,
"tokenType": "Bearer"
}
// Authenticated request
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// Token refresh
POST /api/token/refresh
{
"refreshToken": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
// Response: New token pair
// Token structure (decoded)
{
"sub": "123",
"email": "user@example.com",
"roles": ["user"],
"iat": 1642694400,
"exp": 1642695300,
"iss": "my-api",
"aud": "my-app"
}
Limitations
- Tokens can’t be invalidated without blacklist
- Requires secure secret/key management
- Token size can be large with many claims
- Clock skew between servers needs handling
Plugin: logging
Request/response logging for debugging and monitoring.
Introduction
The Logging plugin provides comprehensive request and response logging with support for multiple formats and destinations. Essential for debugging, monitoring, and audit trails.
What json-rest-api provides
- Structured logging (JSON, logfmt, Apache)
- Request/response capture
- Performance metrics
- Error tracking
- Log levels and filtering
- Multiple destinations
- Privacy controls (PII masking)
- Correlation IDs
How to use it
import { LoggingPlugin } from 'json-rest-api';
api.use(LoggingPlugin, {
// Log level
level: 'info', // debug, info, warn, error
// What to log
logRequests: true,
logResponses: true,
logErrors: true,
logPerformance: true,
// Format
format: 'json', // json, logfmt, apache, dev
// Privacy
redactFields: ['password', 'token', 'ssn'],
redactHeaders: ['Authorization', 'Cookie'],
// Performance
slowRequestThreshold: 1000, // Log slow requests (ms)
// Output
transports: [
{ type: 'console', level: 'info' },
{ type: 'file', filename: 'api.log', level: 'error' },
{ type: 'syslog', host: 'logs.example.com', level: 'warn' }
],
// Custom formatter
formatter: (log) => {
if (log.level === 'error') {
return `🚨 ${log.message}`;
}
return JSON.stringify(log);
}
});
What to expect
// Request log
{
"timestamp": "2024-01-20T10:00:00Z",
"level": "info",
"type": "request",
"requestId": "req-123",
"method": "POST",
"path": "/api/users",
"ip": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"userId": "456"
}
// Response log
{
"timestamp": "2024-01-20T10:00:01Z",
"level": "info",
"type": "response",
"requestId": "req-123",
"statusCode": 201,
"duration": 145,
"size": 1234
}
// Error log
{
"timestamp": "2024-01-20T10:00:02Z",
"level": "error",
"type": "error",
"requestId": "req-124",
"error": {
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"stack": "Error: Validation failed\n at..."
},
"context": {
"path": "/api/posts",
"method": "POST",
"userId": "789"
}
}
// Performance warning
{
"timestamp": "2024-01-20T10:00:03Z",
"level": "warn",
"type": "performance",
"message": "Slow request detected",
"duration": 2543,
"path": "/api/reports/generate",
"threshold": 1000
}
Limitations
- High-volume logging impacts performance
- Log files need rotation management
- Sensitive data requires careful handling
- Storage costs can be significant
Plugin: migration-plugin
Database schema migrations with version control.
Introduction
The Migration plugin provides a robust system for managing database schema changes over time. It tracks applied migrations and supports both forward migrations and rollbacks.
What json-rest-api provides
- Automatic migration discovery
- Transaction support
- Up and down migrations
- Batch tracking
- Schema helpers
- Safe concurrent execution
- Migration generator
- Status reporting
How to use it
import { MigrationPlugin } from 'json-rest-api';
api.use(MigrationPlugin, {
directory: './migrations', // Migration files location
table: '_migrations', // Tracking table name
});
// Create a migration file
// migrations/20240120100000_create_users_table.js
export default {
async up(db) {
await db.createTable('users', {
id: { type: 'id', primaryKey: true },
email: { type: 'string', length: 255, unique: true },
name: { type: 'string', length: 100 },
created_at: { type: 'timestamp', default: 'CURRENT_TIMESTAMP' }
});
await db.addIndex('users', ['email']);
},
async down(db) {
await db.dropTable('users');
}
};
// Run migrations
await api.migrate(); // Run all pending
await api.migrate({ to: '20240120100000' }); // Migrate to specific version
// Rollback
await api.rollback(); // Rollback last batch
await api.rollback({ steps: 3 }); // Rollback 3 migrations
// Status
const status = await api.migrationStatus();
console.log('Applied:', status.applied);
console.log('Pending:', status.pending);
// Generate migration
await api.generateMigration('add_user_roles', {
up: `
ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT 'user';
CREATE INDEX idx_users_role ON users(role);
`,
down: `
DROP INDEX idx_users_role;
ALTER TABLE users DROP COLUMN role;
`
});
What to expect
// Migration file structure
migrations/
├── 20240120100000_create_users_table.js
├── 20240120110000_add_user_roles.js
└── 20240120120000_create_posts_table.js
// Running migrations
$ npm run migrate
Running migration: 20240120100000_create_users_table
Running migration: 20240120110000_add_user_roles
Running migration: 20240120120000_create_posts_table
Completed 3 migrations
// Status check
$ npm run migrate:status
Applied migrations:
- 20240120100000_create_users_table.js (batch 1)
- 20240120110000_add_user_roles.js (batch 1)
Pending migrations:
- 20240120120000_create_posts_table.js
// Rollback
$ npm run migrate:rollback
Rolling back: 20240120110000_add_user_roles
Rolling back: 20240120100000_create_users_table
Rolled back batch 1 (2 migrations)
Limitations
- Only supports SQL databases
- Complex migrations may need manual SQL
- Concurrent migrations need coordination
- No automatic conflict resolution
Plugin: query-limits
Prevent resource exhaustion with query limits.
Introduction
The Query Limits plugin protects your API from resource exhaustion by limiting query complexity, result sizes, and nested includes. Essential for public APIs.
What json-rest-api provides
- Maximum page size limits
- Total results cap
- Include depth limits
- Query complexity scoring
- Rate-based limits
- Per-resource configuration
- User-based exceptions
- Monitoring hooks
How to use it
import { QueryLimitsPlugin } from 'json-rest-api';
api.use(QueryLimitsPlugin, {
// Global limits
maxPageSize: 100,
maxTotalResults: 1000,
maxIncludeDepth: 3,
maxFilterComplexity: 10,
// Per-resource overrides
resources: {
'large-datasets': {
maxPageSize: 20,
maxTotalResults: 100
},
'expensive-computations': {
maxPageSize: 10,
disableIncludes: true
}
},
// User-based exceptions
userLimits: (user) => {
if (user?.subscription === 'premium') {
return {
maxPageSize: 500,
maxTotalResults: 5000
};
}
},
// Complexity scoring
complexityScoring: {
filter: 1, // Per filter
sort: 2, // Per sort field
include: 3, // Per include
nestedFilter: 5 // Per nested filter
},
// Monitoring
onLimitExceeded: async (limit, context) => {
await logMetric('query_limit_exceeded', {
limit: limit.type,
resource: context.options.type,
user: context.options.user?.id
});
}
});
What to expect
// Request exceeding page size limit
GET /api/posts?page[size]=1000
// Response: 400 Bad Request
{
"error": "Page size exceeds maximum of 100"
}
// Automatic size reduction
GET /api/posts?page[size]=200
// Size automatically reduced to 100
// Deep include prevention
GET /api/posts?include=author.company.country.continent.planet
// Response: 400 Bad Request
{
"error": "Include depth exceeds maximum of 3"
}
// Complexity limit
GET /api/posts?filter[title]=test&filter[content]=lorem&filter[authorId]=1&sort=-date,-views&include=author,comments,tags
// Response: 400 Bad Request
{
"error": "Query complexity score 15 exceeds maximum of 10"
}
// Premium user with higher limits
GET /api/posts?page[size]=500
Authorization: Bearer [premium-user-token]
// Success - premium users have higher limits
Limitations
- Can frustrate users with legitimate needs
- Complexity scoring is approximate
- Doesn’t prevent all DoS attacks
- May need tuning per use case
Plugin: security
Security headers and protections.
Introduction
The Security plugin adds essential HTTP security headers and protections to prevent common web vulnerabilities like XSS, clickjacking, and MIME sniffing.
What json-rest-api provides
- Security headers (CSP, HSTS, etc.)
- XSS protection
- Clickjacking prevention
- MIME type sniffing prevention
- Referrer policy
- Permissions policy
- Certificate transparency
- Report-only mode for testing
How to use it
import { SecurityPlugin } from 'json-rest-api';
api.use(SecurityPlugin, {
// Content Security Policy
csp: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.example.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"]
},
reportUri: '/api/csp-report'
},
// Strict Transport Security
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
// Other headers
xFrameOptions: 'DENY',
xContentTypeOptions: 'nosniff',
xXssProtection: '1; mode=block',
referrerPolicy: 'strict-origin-when-cross-origin',
// Permissions Policy
permissionsPolicy: {
camera: 'none',
microphone: 'none',
geolocation: 'self',
payment: 'self'
},
// Certificate Transparency
expectCt: {
maxAge: 86400,
enforce: true,
reportUri: '/api/ct-report'
},
// Development mode
development: {
csp: { reportOnly: true }, // Don't block in dev
hsts: { enabled: false } // No HSTS in dev
}
});
What to expect
Response headers added:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; ...
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=(self)
Expect-CT: max-age=86400, enforce, report-uri="/api/ct-report"
CSP violation report:
POST /api/csp-report
{
"csp-report": {
"blocked-uri": "https://evil.com/script.js",
"disposition": "enforce",
"document-uri": "https://myapp.com/page",
"violated-directive": "script-src"
}
}
Limitations
- CSP can break legitimate functionality
- Some headers not supported by all browsers
- Report endpoints can be spammed
- HSTS is hard to undo
Plugin: versioning
API versioning with backward compatibility.
Introduction
The Versioning plugin enables you to maintain multiple API versions simultaneously, allowing gradual client migration and backward compatibility.
What json-rest-api provides
- URL-based versioning (/v1, /v2)
- Header-based versioning
- Query parameter versioning
- Version negotiation
- Deprecation warnings
- Version-specific schemas
- Migration helpers
- Version analytics
How to use it
import { VersioningPlugin } from 'json-rest-api';
api.use(VersioningPlugin, {
// Versioning strategy
strategy: 'url', // url, header, query
headerName: 'API-Version',
queryParam: 'version',
// Versions
versions: {
'v1': {
deprecated: true,
deprecationDate: '2024-12-31',
schemas: {
users: schemaV1 // Old schema
}
},
'v2': {
current: true,
schemas: {
users: schemaV2 // New schema with changes
}
}
},
// Version negotiation
defaultVersion: 'v2',
strict: false, // Allow missing version
// Transformation between versions
transformers: {
'users': {
'v1->v2': (data) => ({
...data,
fullName: `${data.firstName} ${data.lastName}`,
firstName: undefined,
lastName: undefined
}),
'v2->v1': (data) => {
const [firstName, ...rest] = data.fullName.split(' ');
return {
...data,
firstName,
lastName: rest.join(' '),
fullName: undefined
};
}
}
}
});
// Different ways to specify version
// URL: GET /v1/users
// Header: GET /users (with API-Version: v1)
// Query: GET /users?version=v1
// Register version-specific routes
api.version('v1').addResource('users', userSchemaV1);
api.version('v2').addResource('users', userSchemaV2);
// Deprecation notices
api.version('v1').deprecate({
message: 'Version 1 is deprecated. Please migrate to v2.',
sunset: '2024-12-31',
migrationGuide: 'https://docs.example.com/migration'
});
What to expect
// Version 1 request
GET /v1/users/123
// Response includes deprecation warning:
{
"data": {
"id": "123",
"type": "users",
"attributes": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com"
}
},
"meta": {
"deprecation": {
"message": "Version 1 is deprecated. Please migrate to v2.",
"sunset": "2024-12-31",
"migrationGuide": "https://docs.example.com/migration"
}
}
}
// Headers:
// Sunset: Sat, 31 Dec 2024 23:59:59 GMT
// Deprecation: version="v1"
// Version 2 request (same user)
GET /v2/users/123
{
"data": {
"id": "123",
"type": "users",
"attributes": {
"fullName": "John Doe", // Combined field
"email": "john@example.com"
}
}
}
// Version negotiation
GET /users/123
Accept: application/vnd.api+json; version=2
// Returns v2 format
Limitations
- Maintaining multiple versions increases complexity
- Transformers can be error-prone
- Storage overhead for version-specific data
- Testing burden multiplies
Plugin: views
Custom data transformations and projections.
Introduction
The Views plugin allows you to create custom representations of your data with different fields, formats, and access levels. Perfect for role-based data visibility and API response optimization.
What json-rest-api provides
- Named views with custom field sets
- Role-based view selection
- Field transformation and computed values
- View inheritance
- Performance optimization
- Format conversion
- Nested view support
How to use it
import { ViewsPlugin } from 'json-rest-api';
api.use(ViewsPlugin);
// Define views for users resource
api.defineViews('users', {
// Public view - limited fields
public: {
fields: ['id', 'name', 'avatar'],
transform: {
avatar: (value) => value || '/default-avatar.png'
}
},
// Authenticated view - more fields
authenticated: {
extends: 'public',
fields: ['+email', '+createdAt'], // + means add to parent
computed: {
memberSince: (user) => new Date(user.createdAt).getFullYear()
}
},
// Admin view - all fields plus extras
admin: {
extends: 'authenticated',
fields: ['*'], // All fields
computed: {
lastLoginDays: (user) => {
const days = (Date.now() - new Date(user.lastLogin)) / 86400000;
return Math.floor(days);
}
},
include: {
loginHistory: {
view: 'summary', // Use summary view for nested data
limit: 5
}
}
},
// Profile view - for user's own data
profile: {
extends: 'authenticated',
fields: ['+phone', '+address', '+settings'],
transform: {
phone: (value, user, context) => {
// Mask phone for privacy
return value.replace(/\d{4}$/, '****');
}
}
}
});
// Use views in queries
const publicUsers = await api.resources.users.query({
view: 'public'
});
// Automatic view selection based on role
api.hook('beforeSend', async (context) => {
if (context.options.type === 'users' && !context.params?.view) {
const user = context.options.user;
if (!user) {
context.params.view = 'public';
} else if (user.role === 'admin') {
context.params.view = 'admin';
} else if (context.result?.id === user.id) {
context.params.view = 'profile';
} else {
context.params.view = 'authenticated';
}
}
});
What to expect
// Public view
GET /api/users/123?view=public
{
"data": {
"id": "123",
"type": "users",
"attributes": {
"name": "John Doe",
"avatar": "/default-avatar.png"
}
}
}
// Authenticated view
GET /api/users/123?view=authenticated
Authorization: Bearer [token]
{
"data": {
"id": "123",
"type": "users",
"attributes": {
"name": "John Doe",
"avatar": "/uploads/john.jpg",
"email": "john@example.com",
"createdAt": "2020-01-15T10:00:00Z",
"memberSince": 2020
}
}
}
// Admin view with nested data
GET /api/users/123?view=admin
Authorization: Bearer [admin-token]
{
"data": {
"id": "123",
"type": "users",
"attributes": {
"name": "John Doe",
"email": "john@example.com",
"role": "user",
"status": "active",
"lastLogin": "2024-01-19T15:30:00Z",
"lastLoginDays": 1,
// ... all other fields
},
"relationships": {
"loginHistory": {
"data": [...]
}
}
}
}
Limitations
- Complex views can impact performance
- View logic can become hard to maintain
- No automatic view discovery for clients
- Cache invalidation complexity
Summary
The core-extra plugins provide essential functionality for production applications:
- Security: audit-log, authorization, csrf, jwt, security
- API Features: computed, cors, versioning, views
- Data Management: migration-plugin, query-limits
- Operations: logging
Each plugin is designed to work independently or together, allowing you to add only what you need. Most provide sensible defaults while allowing deep customization when required.
For protocol support (GraphQL, WebSocket, etc.), see the Protocol Plugins documentation.