Interceptors

Learn how to use interceptors to modify, monitor, or add custom logic to requests and responses in your API.

What Are Interceptors?

Interceptors are powerful hooks that allow you to intercept and modify:

  • Incoming requests before they are processed
  • Outgoing responses before they are returned to the client

This enables you to add custom logic such as:

  • Authentication and authorization
  • Request validation and transformation
  • Response modification
  • Logging and monitoring
  • Error handling
  • Data customization

Types of Interceptors

api.vision supports two main types of interceptors:

Request Interceptors

Execute before the request is processed. Can modify the request or short-circuit the response.

Use cases: Authentication, request validation, request transformation

Response Interceptors

Execute after the response is generated but before it's sent to the client.

Use cases: Response transformation, adding headers, error handling

Creating Interceptors

Interceptors can be defined in your API configuration or through the dashboard:

Request Interceptor Example

{
  "interceptors": {
    "request": [
      {
        "name": "authCheck",
        "path": "**", // Apply to all paths
        "script": "
          // Check for API key
          const apiKey = req.headers['x-api-key'];
          if (!apiKey || apiKey !== 'your-secret-key') {
            return {
              status: 401,
              body: { 
                error: 'Unauthorized',
                message: 'Invalid or missing API key'
              }
            };
          }
          
          // Add user info to the request for downstream use
          req.locals.user = { id: 42, role: 'admin' };
          
          // Continue with the request
          return null;
        "
      }
    ]
  }
}

Response Interceptor Example

{
  "interceptors": {
    "response": [
      {
        "name": "addHeaders",
        "path": "**", // Apply to all paths
        "script": "
          // Add custom headers to all responses
          res.headers['x-api-version'] = '1.0.0';
          res.headers['x-response-time'] = `${Date.now() - req.startTime}ms`;
          
          // Log the response
          console.log(`Response to ${req.method} ${req.path}: ${res.status}`);
          
          // Return the modified response
          return res;
        "
      }
    ]
  }
}

Interceptor Context

Interceptors have access to a rich context with these objects:

ObjectAvailable InDescription
reqBothThe request object with method, path, headers, query params, body
resResponse onlyThe response object with status, headers, body
configBothThe API configuration
storeBothPersistent storage for the API
utilsBothUtility functions for common operations

The Request Object

req = {
  method: 'GET',             // HTTP method
  path: '/users/42',         // Path without query string
  pathParams: { id: '42' },  // Path parameters
  query: { fields: 'name,email' }, // Query parameters
  headers: { ... },          // Request headers (lowercase keys)
  body: { ... },             // Request body (parsed if JSON)
  cookies: { ... },          // Request cookies
  ip: '192.168.1.1',         // Client IP address
  startTime: 1618497730000,  // Request start time (timestamp)
  locals: { ... }            // Local variables for this request
}

The Response Object

res = {
  status: 200,             // HTTP status code
  headers: { ... },        // Response headers
  body: { ... },           // Response body
  cookies: { ... },        // Response cookies to set
  locals: { ... }          // Local variables for this response
}

Common Interceptor Use Cases

Authentication and Authorization

// JWT Authentication Interceptor
{
  "name": "jwtAuth",
  "path": "**",
  "script": "
    // Skip auth for login endpoint
    if (req.path === '/login') return null;
    
    // Get token from Authorization header
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return { status: 401, body: { error: 'Unauthorized' } };
    }
    
    const token = authHeader.split(' ')[1];
    
    try {
      // Verify JWT (in a real app, use a proper JWT library)
      const user = utils.jwt.verify(token, 'your-secret-key');
      
      // Add user to request context
      req.locals.user = user;
      
      // Check authorization for admin-only endpoints
      if (req.path.startsWith('/admin') && user.role !== 'admin') {
        return { status: 403, body: { error: 'Forbidden' } };
      }
      
      return null; // Continue processing the request
    } catch (err) {
      return { status: 401, body: { error: 'Invalid token' } };
    }
  "
}

Request Transformation

// Request Transformation Interceptor
{
  "name": "transformRequest",
  "path": "/products",
  "methods": ["POST", "PUT", "PATCH"],
  "script": "
    // Convert snake_case to camelCase in request body
    if (req.body && typeof req.body === 'object') {
      req.body = utils.transformKeys(req.body, utils.snakeToCamel);
    }
    
    // Add timestamp to request
    if (req.method === 'POST') {
      req.body.createdAt = new Date().toISOString();
    }
    
    req.body.updatedAt = new Date().toISOString();
    
    return null; // Continue with the modified request
  "
}

Response Transformation

// Response Transformation Interceptor
{
  "name": "transformResponse",
  "path": "/products/**",
  "methods": ["GET"],
  "script": "
    // Convert camelCase to snake_case in response body
    if (res.body) {
      if (Array.isArray(res.body)) {
        res.body = res.body.map(item => utils.transformKeys(item, utils.camelToSnake));
      } else {
        res.body = utils.transformKeys(res.body, utils.camelToSnake);
      }
    }
    
    // Add API version header
    res.headers['x-api-version'] = '1.0.0';
    
    return res;
  "
}

Error Handling

// Error Handling Interceptor
{
  "name": "errorHandler",
  "path": "**",
  "script": "
    // Skip if the response is already set and is not an error
    if (res && res.status < 400) return res;
    
    // Standardize error format
    if (res && res.status >= 400) {
      const originalBody = res.body;
      res.body = {
        status: res.status,
        error: utils.getHttpStatusText(res.status),
        message: originalBody?.message || originalBody?.error || &apos;An error occurred&apos;,
        path: req.path,
        timestamp: new Date().toISOString()
      };
      
      // Log the error
      console.error(`Error in ${req.method} ${req.path}: ${res.status} ${res.body.message}`);
      
      return res;
    }
    
    return null;
  "
}

Conditional Logic and Dynamic Responses

// Dynamic Response Interceptor
{
  "name": "dynamicResponse",
  "path": "/weather",
  "methods": ["GET"],
  "script": "
    // Get location from query params
    const location = req.query.location || &apos;default&apos;;
    
    // Simulate different weather based on location
    let weather;
    
    if (location.toLowerCase().includes(&apos;seattle&apos;)) {
      weather = { condition: &apos;rainy&apos;, temperature: 12, unit: &apos;C&apos; };
    } else if (location.toLowerCase().includes(&apos;desert&apos;)) {
      weather = { condition: &apos;sunny&apos;, temperature: 35, unit: &apos;C&apos; };
    } else if (location.toLowerCase().includes(&apos;mountain&apos;)) {
      weather = { condition: &apos;snowy&apos;, temperature: -5, unit: &apos;C&apos; };
    } else {
      weather = { condition: &apos;partly cloudy&apos;, temperature: 22, unit: &apos;C&apos; };
    }
    
    // Return custom response immediately
    return {
      status: 200,
      body: {
        location,
        weather,
        forecast: [&apos;similar&apos;, &apos;similar&apos;, &apos;improving&apos;, &apos;better&apos;, &apos;nice&apos;],
        lastUpdated: new Date().toISOString()
      }
    };
  "
}

Advanced Interceptor Features

Path Matching

Interceptors use glob pattern matching for the path property:

PatternDescriptionExamples
**Matches any path/users, /products/123, etc.
/users/*Matches one level under /users/users/123, /users/profile, etc.
/users/**Matches any path under /users/users/123, /users/123/posts, etc.
/users/:idMatches paths with named parameter/users/123, /users/abc, etc.

Interceptor Order and Priority

When multiple interceptors match a request, they execute in order of:

  1. Global interceptors (path = "**")
  2. More specific path matches (from least to most specific)
  3. Order in the configuration (first to last)

You can explicitly set priority to override the default order:

{
  "name": "highPriorityInterceptor",
  "path": "/users",
  "priority": 100, // Higher priority executes first
  "script": "..."
}

Async Operations

Interceptors support async operations using async/await:

{
  "name": "externalApiInterceptor",
  "path": "/weather/:city",
  "script": "
    // Simulate fetching data from external API
    async function getWeatherData(city) {
      // In a real interceptor, this would call an actual API
      await utils.sleep(500); // Simulate network delay
      
      return {
        city,
        temperature: Math.floor(Math.random() * 30) + 5,
        condition: [&apos;sunny&apos;, &apos;cloudy&apos;, &apos;rainy&apos;, &apos;snowy&apos;][Math.floor(Math.random() * 4)]
      };
    }
    
    // Execute the async function and return the result
    const city = req.pathParams.city;
    const weatherData = await getWeatherData(city);
    
    return {
      status: 200,
      body: weatherData
    };
  "
}

Best Practices

  • Keep interceptors focused on a single responsibility
  • Use descriptive names that reflect what the interceptor does
  • Add comments to explain complex logic
  • Be careful with performance in interceptors that run on every request
  • Use try/catch blocks to handle errors gracefully
  • Test interceptors thoroughly with different inputs
  • Consider security implications, especially when modifying authentication or authorization logic