JSON REST API

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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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

Summary

The core-extra plugins provide essential functionality for production applications:

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.