JSON REST API

Hooks and Data Management

This guide provides a comprehensive reference for all hooks available in the json-rest-api system, including system-wide hooks from hooked-api and method-specific hooks from the REST API plugin.

An important concept when working with hooks is understanding how simplified mode affects data flow.

Inside hooks, JSON:API format is king regardless of simplified mode. Simplified mode only ever affects:

This means when writing hooks, you always work with the standard JSON:API structure:

// In a hook, the record is ALWAYS JSON:API format:
hooks: {
  beforeData: async ({ context }) => {
    // Even in simplified mode, inputRecord has JSON:API structure
    if (context.method === 'post' && context.inputRecord) {
      // Always access via data.attributes
      context.inputRecord.data.attributes.created_at = new Date().toISOString();
    }
  },
  
  // IMPORTANT: Use enrichAttributes to modify attributes, NOT enrichRecord
  enrichAttributes: async ({ context }) => {
    // This is called for ALL records (main and included/child records)
    // Add computed fields directly to context.attributes
    context.attributes.computed_field = 'value';
    context.attributes.word_count = context.attributes.content?.split(' ').length || 0;
  }
}

One of the main practical use of hooks is to manupulate data before it’s committed to the database.

Customizing the API as a whole with customize()

The customize() method is the primary way to extend your API with hooks, variables, and helper functions. This method is available on the API instance and provides a cleaner alternative to calling individual methods like addHook().

The customize() method accepts an object with the following properties:

Basic Example

The customize() method accepts an object with hooks, vars (shared state), helpers (reusable functions), apiMethods (global methods), and scopeMethods (methods for all scopes):

api.customize({
  // Shared variables accessible throughout the API
  vars: {
    appName: 'My Application',
    userRoles: ['admin', 'editor', 'viewer'],
    environment: process.env.NODE_ENV
  },
  
  // Reusable helper functions
  helpers: {
    formatDate: (date) => new Date(date).toLocaleDateString(),
    validateEmail: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
    hashPassword: async (password) => {
      const salt = await bcrypt.genSalt(10);
      return bcrypt.hash(password, salt);
    }
  },
  
  // Hooks for customizing behavior
  hooks: {
    beforeData: async ({ context, vars, helpers, log }) => {
      log.info(`${context.method} operation on ${context.scopeName}`);
      
      // Use vars for configuration
      if (vars.environment === 'production') {
        // Production-specific logic
      }
      
      // Modify data for POST requests
      if (context.method === 'post' && context.inputRecord) {
        // Set timestamps
        context.inputRecord.data.attributes.created_at = new Date().toISOString();
        
        // Validate and transform data using helpers
        if (context.scopeName === 'users') {
          // Hash password
          if (context.inputRecord.data.attributes.password) {
            context.inputRecord.data.attributes.password = await helpers.hashPassword(
              context.inputRecord.data.attributes.password
            );
          }
        
        }
      }
    },
    
    enrichAttributes: async ({ context }) => {
      // Add computed fields to posts
      if (context.scopeName === 'posts') {
        // NOTE that 'preview' MUST be an existing record on the database
        context.attributes.preview = context.attributes.content?.substring(0, 200) + '...';
      }
    }
  },
  
  // API-level methods: api.methodName()
  apiMethods: {
    healthCheck: async ({ vars, scopes }) => ({
      status: 'ok',
      version: vars.apiVersion || '1.0.0',
      environment: vars.environment,
      timestamp: new Date().toISOString(),
      resources: Object.keys(scopes)
    })
  },
  
  // Scope methods: api.scopes.resourceName.methodName()
  scopeMethods: {
    count: async ({ scopeName, db }) => {
      const result = await db(scopeName).count('* as total').first();
      return result.total;
    }
  }
});

// Usage examples:
const health = await api.healthCheck();
const userCount = await api.scopes.users.count();

Important: Hooks added via customize() are registered with the plugin name api-custom:{apiName} for tracking purposes.

Customizing Resources with extras (per resource)

While customize() applies globally to all resources, you can customize individual resources using the extras parameter in addScope(). This provides resource-specific hooks, methods, variables, and helpers that apply only to that particular resource.

Using extras in addScope

api.addScope('posts', {
  // Schema and options
}, {
  // extras parameter - everything here applies ONLY to posts
  hooks: {

    // This will change data before getting posted
    beforeDataPost: async ({ context }) => {
      const attributes = context.inputRecord.data.attributes
      // This hook only runs for POST operations on posts
      attributes.created_at = new Date().toISOString();
    },
    
    // This will enrich all attributes (main and included) when fetched
    // It's for adding computed fields to the RESPONSE, NOT for saving to database
    // These computed fields are sent to the client but never stored 
    enrichAttributes: async ({ context }) => {  
      // This wordCount is calculated on-the-fly and included in the response
      // It is NOT saved to the database
      context.attributes.wordCount = context.attributes.content?.split(' ').length || 0;
      
      // Example: Add a reading time estimate (also not stored)
      const wordsPerMinute = 200;
      context.attributes.readingTimeMinutes = Math.ceil(context.attributes.wordCount / wordsPerMinute);
    }
  },
  
  // CLAUDE: NO. This should use patch, since direct database operations
  // will skip the lifecycle (e.g. Sockets notifications etc.)
  scopeMethods: {
    // This method is ONLY available on posts: api.scopes.posts.publish()
    publish: async ({ params, context, db, runHooks }) => {
      const id = params.id;
      const result = await db('posts')
        .where('id', id)
        .update({ 
          status: 'published',
          published_at: new Date().toISOString()
        });
      return { success: true, published: result };
    },
    
    // Only on posts: api.scopes.posts.findByAuthor()
    findByAuthor: async ({ params, scope }) => {
      return await scope.query({
        filters: { author_id: params.authorId }
      });
    }
  },
  
  vars: {
    // Resource-specific configuration
    maxTitleLength: 200,
    allowedStatuses: ['draft', 'published', 'archived'],
    defaultStatus: 'draft'
  },
  
  helpers: {
    // Resource-specific helper functions
    generateSlug: (title) => {
      return title.toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '');
    },
    
    validateStatus: (status, vars) => {
      // Note: can access vars through second parameter
      return vars.allowedStatuses.includes(status);
    }
  }
});

Variable and Helper Fallback System

An important feature of resource-specific vars and helpers is the fallback system:

  1. Variables (vars): When you access a variable in a resource context, it first checks the resource’s vars. If not found, it falls back to the global API vars.
// Global vars
api.customize({
  vars: {
    appName: 'My Blog',
    defaultPageSize: 20,
    maxUploadSize: 5242880  // 5MB
  }
});

// Resource-specific vars
api.addScope('posts', {}, {
  vars: {
    defaultPageSize: 10,  // Override for posts only
    maxTitleLength: 200   // Posts-specific var
  }
});

// In a posts hook or method:
// vars.defaultPageSize → 10 (from posts vars)
// vars.maxUploadSize → 5242880 (fallback to global)
// vars.maxTitleLength → 200 (posts-specific)
// vars.appName → 'My Blog' (fallback to global)
  1. Helpers: Same fallback behavior - resource helpers are checked first, then global helpers.
// Global helpers
api.customize({
  helpers: {
    formatDate: (date) => new Date(date).toLocaleDateString(),
    sanitizeHtml: (html) => { /* ... */ }
  }
});

// Resource-specific helpers
api.addScope('posts', {}, {
  helpers: {
    formatDate: (date) => new Date(date).toISOString(), // Override for posts
    generateExcerpt: (content) => content.substring(0, 150) + '...'
  }
});

// In posts context:
// helpers.formatDate() → uses posts version (ISO format)
// helpers.sanitizeHtml() → uses global version (fallback)
// helpers.generateExcerpt() → posts-specific helper

This means that you are able to specify api-wide variables and helpers, but can then override them by resource.

Resource-Specific vs Global Customization

Feature Global (customize()) Resource-Specific (extras)
Scope Applies to all resources Applies to one resource only
Hooks Must check context.scopeName Automatically scoped
Methods apiMethodsapi.methodName()
scopeMethods → all scopes
scopeMethods → only this scope
Vars Global defaults Resource-specific with fallback
Helpers Global utilities Resource-specific with fallback

Best Practices for Resource Customization

  1. Use extras for resource-specific logic - Don’t clutter global hooks with scopeName checks
  2. Leverage the fallback system - Define common utilities globally, override when needed
  3. Keep resource methods focused - Methods should relate to that specific resource
  4. Document resource-specific vars - Make it clear what configuration is available
  5. Avoid naming conflicts - Be aware that resource vars/helpers can shadow global ones

Best Practices for Global Customization

  1. Check scopeName in Hooks - Since customize() is API-level only, use context.scopeName to implement resource-specific logic
  2. Keep Helpers Pure - Make helpers independent functions that are easier to test and reuse
  3. Use Vars for Configuration - Store configuration values in vars instead of hardcoding them
  4. Avoid Mutable Shared State - Be careful with objects/arrays in vars as they’re shared across all requests
  5. Handle Errors Gracefully - Thrown errors in hooks will stop the operation and return to the client
  6. Use Method-Specific Hooks - Use beforeDataPost, afterDataPatch, etc. for operation-specific logic

REST API Method Hooks

These hooks are triggered by the REST API plugin during CRUD operations. Each method follows a consistent pattern but with method-specific variations.

Important Context Properties

The context object contains different properties depending on the method and stage of execution:

Common Properties:

Write Operation Properties (POST/PUT/PATCH):

Query/Read Properties:

Hook Execution Pattern

All REST API methods follow this general pattern:

  1. Before hooks - Run before the main operation
  2. Permission checks - For GET method
  3. Main operation - The actual database operation
  4. After hooks - Run after the operation
  5. Enrichment hooks - To enhance the record
  6. Transaction hooks - Commit/rollback for write operations
  7. Finish hooks - Final cleanup/processing

QUERY Method Hooks

Used for retrieving collections of resources with filtering, sorting, and pagination.

beforeData

When: Before executing the database query
Purpose: Modify query parameters, add custom filters, set defaults

Context contains:

What can be changed:

Example:

// In api.addScope('posts', {}, extras):
hooks: {
  beforeData: async ({ context }) => {
    if (context.method === 'query' && context.auth?.userId) {
      // Only show posts by the current user
      context.queryParams.filters = {
        ...context.queryParams.filters,
        author_id: context.auth.userId
      };
    }
  }
}

beforeDataQuery

When: Immediately after beforeData, query-specific
Purpose: Query-specific modifications

Context: Same as beforeData
What can be changed: Same as beforeData

enrichRecord

When: After data is fetched from database and normalized
Purpose: Modify the response structure, add metadata, or handle response-level concerns

IMPORTANT: Do NOT use this hook to add/modify attributes. Use enrichAttributes instead.

Context contains:

What can be changed:

Example:

hooks: {
  enrichAttributes: async ({ context }) => {
    if (context.parentContext?.method === 'query') {
      // Add computed fields that are NOT stored in database
      // These are calculated fresh for each response
      context.attributes.wordCount = context.attributes.content?.split(' ').length || 0;
      context.attributes.excerpt = context.attributes.content?.substring(0, 150) + '...';
      
      // Transform display values (original remains in database)
      // Database still has lowercase title, this is just for this response
      context.attributes.displayTitle = context.attributes.title?.toUpperCase();
    }
  }
}

finish

When: Before returning the response
Purpose: Final logging, metrics collection

Context contains: All previous context properties
What can be changed: Nothing - hooks should NOT change context.record at this stage

finishQuery

When: Immediately after finish, query-specific
Purpose: Query-specific final processing

Context: Same as finish
What can be changed: Nothing - informational only

GET Method Hooks

Used for retrieving a single resource by ID.

beforeData

When: Before fetching the single resource
Purpose: Modify query parameters, prepare for fetch

Context contains:

What can be changed:

beforeDataGet

When: Immediately after beforeData, get-specific
Context: Same as beforeData
What can be changed: Same as beforeData

checkDataPermissions

When: After the record is fetched, before enrichment
Purpose: Implement row-level security, check access permissions

Context contains:

What can be changed:

Example:

hooks: {
  checkDataPermissions: async ({ context }) => {
    if (context.method === 'get') {
      const post = context.record.data;
      if (post.attributes.status === 'draft' && post.attributes.author_id !== context.auth?.userId) {
        throw new Error('Access denied: Cannot view draft posts by other authors');
      }
    }
  }
}

checkDataPermissionsGet

When: Immediately after checkDataPermissions, get-specific
Context: Same as checkDataPermissions
What can be changed: Same as checkDataPermissions

enrichRecord

When: After permission checks
Purpose: Modify the response structure or add metadata

IMPORTANT: Do NOT use this hook to add/modify attributes. Use enrichAttributes instead.

Context contains:

What can be changed:

enrichRecordWithRelationships

When: After basic enrichment
Purpose: Add relationship metadata, enhance relationship data

Context: Same as enrichRecord
What can be changed:

finish

When: Before returning the response
Context: All accumulated context
What can be changed: Nothing - informational only

finishGet

When: Immediately after finish, get-specific
Context: Same as finish
What can be changed: Nothing - informational only

POST Method Hooks

Used for creating new resources.

beforeData

When: Before creating the new resource
Purpose: Validate data, set defaults, compute values

Context contains:

What can be changed:

Note: After validation, attributes are stored in context.inputRecord.data.attributes, not directly in context.attributes.

Example:

hooks: {
  beforeData: async ({ context }) => {
    if (context.method === 'post' && context.inputRecord) {
      // Set default status
      if (!context.inputRecord.data.attributes.status) {
        context.inputRecord.data.attributes.status = 'draft';
      }
      
      // Set author from auth context (this would typically be in belongsToUpdates)
      if (context.auth?.userId && context.belongsToUpdates) {
        context.belongsToUpdates.author_id = context.auth.userId;
      }
      
      // Add creation timestamp
      context.inputRecord.data.attributes.created_at = new Date().toISOString();
    }
  }
}

beforeDataPost

When: Immediately after beforeData, post-specific
Context: Same as beforeData
What can be changed: Same as beforeData

afterData

When: After the resource is created in the database
Purpose: Trigger side effects, create related records

Context contains:

What can be changed:

Example:

hooks: {
  afterData: async ({ context, scopes }) => {
    if (context.method === 'post') {
      // Create a notification for new post
      await scopes.notifications.create({
        type: 'new_post',
        post_id: context.id,
        user_id: context.belongsToUpdates.author_id,
        created_at: new Date().toISOString()
      });
    }
  }
}

afterDataPost

When: Immediately after afterData, post-specific
Context: Same as afterData
What can be changed: Same as afterData

enrichRecord

When: After fetching the created record (if returnFullRecord is not ‘no’)
Purpose: Modify response structure or add metadata

IMPORTANT: Do NOT use this hook to add/modify attributes. Use enrichAttributes instead.

Context contains:

What can be changed:

afterCommit

When: After the transaction is committed (only if shouldCommit is true)
Purpose: Trigger post-commit side effects like sending emails, webhooks

Context: All accumulated context
What can be changed: Nothing - for side effects only

Example:

hooks: {
  afterCommit: async ({ context, helpers }) => {
    if (context.method === 'post') {
      // Send email notification (safe to do after commit)
      await helpers.emailService.send({
        template: 'new_post',
        data: {
          postId: context.id,
          title: context.inputRecord.data.attributes.title
        }
      });
    }
  }
}

afterRollback

When: If an error occurs and transaction is rolled back
Purpose: Clean up any external resources, log failures

Context: All accumulated context plus error information
What can be changed: Nothing - for cleanup/logging only

finish

When: Before returning the response
Context: All accumulated context
What can be changed: Nothing - informational only

finishPost

When: Immediately after finish, post-specific
Context: Same as finish
What can be changed: Nothing - informational only

PUT Method Hooks

Used for completely replacing a resource.

beforeData

When: Before replacing the resource
Purpose: Validate replacement data, check permissions

Context contains:

What can be changed:

Note: After validation, attributes are stored in context.inputRecord.data.attributes.

Example:

hooks: {
  beforeData: async ({ context }) => {
    if (context.method === 'put' && context.inputRecord) {
      // Prevent changing the author (check if belongsTo relationship changed)
      const newAuthorId = context.belongsToUpdates?.author_id;
      const currentAuthorId = context.minimalRecord?.data?.relationships?.author?.data?.id;
      if (newAuthorId && newAuthorId !== currentAuthorId) {
        throw new Error('Cannot change post author');
      }
      
      // Add update timestamp
      context.inputRecord.data.attributes.updated_at = new Date().toISOString();
    }
  }
}

beforeDataPut

When: Immediately after beforeData, put-specific
Context: Same as beforeData
What can be changed: Same as beforeData

afterData

When: After the resource is updated and relationships are replaced
Purpose: Handle relationship changes, trigger updates

Context contains:

What can be changed:

afterDataPut

When: Immediately after afterData, put-specific
Context: Same as afterData
What can be changed: Same as afterData

enrichRecord

When: After fetching the updated record (if returnFullRecord is not ‘no’)
Purpose: Modify response structure or add metadata

IMPORTANT: Do NOT use this hook to add/modify attributes. Use enrichAttributes instead.

Context contains:

What can be changed:

enrichRecordWithRelationships

When: After basic enrichment
Context: Same as enrichRecord
What can be changed:

afterCommit

When: After the transaction is committed
Context: All accumulated context
What can be changed: Nothing - for side effects only

afterRollback

When: If an error occurs and transaction is rolled back
Context: All accumulated context plus error information
What can be changed: Nothing - for cleanup only

finish

When: Before returning the response
Context: All accumulated context
What can be changed: Nothing - informational only

finishPut

When: Immediately after finish, put-specific
Context: Same as finish
What can be changed: Nothing - informational only

PATCH Method Hooks

Used for partially updating a resource.

beforeData

When: Before partially updating the resource
Purpose: Validate partial updates, compute derived values

Context contains:

What can be changed:

Note: For PATCH, context.inputRecord.data.attributes contains only the fields being updated. Use context.minimalRecord.data.attributes to access the complete current record.

Example:

hooks: {
  beforeData: async ({ context }) => {
    if (context.method === 'patch' && context.inputRecord) {
      // If status is being changed to published, set publish date
      if (context.inputRecord.data.attributes.status === 'published' && 
          context.minimalRecord?.data?.attributes?.status !== 'published') {
        context.inputRecord.data.attributes.published_at = new Date().toISOString();
      }
      
      // Always update the modified timestamp
      context.inputRecord.data.attributes.updated_at = new Date().toISOString();
    }
  }
}

beforeDataPatch

When: Immediately after beforeData, patch-specific
Context: Same as beforeData
What can be changed: Same as beforeData

afterData

When: After the partial update is applied
Purpose: React to specific changes, trigger conditional side effects

Context contains:

What can be changed:

afterDataPatch

When: Immediately after afterData, patch-specific
Context: Same as afterData
What can be changed: Same as afterData

enrichRecord

When: After fetching the updated record (if returnFullRecord is not ‘no’)
Purpose: Modify response structure or add metadata

IMPORTANT: Do NOT use this hook to add/modify attributes. Use enrichAttributes instead.

Context contains:

What can be changed:

enrichRecordWithRelationships

When: After basic enrichment
Context: Same as enrichRecord
What can be changed:

afterCommit

When: After the transaction is committed
Context: All accumulated context
What can be changed: Nothing - for side effects only

afterRollback

When: If an error occurs and transaction is rolled back
Context: All accumulated context plus error information
What can be changed: Nothing - for cleanup only

finish

When: Before returning the response
Context: All accumulated context
What can be changed: Nothing - informational only

finishPatch

When: Immediately after finish, patch-specific
Context: Same as finish
What can be changed: Nothing - informational only

DELETE Method Hooks

Used for removing resources.

beforeData

When: Before deleting the resource
Purpose: Validate deletion, check for dependencies

Context contains:

What can be changed:

Example:

hooks: {
  beforeData: async ({ context }) => {
    if (context.method === 'delete') {
      // Check if post has comments
      const commentCount = await context.db('comments')
        .where('post_id', context.id)
        .count('* as count')
        .first();
      
      if (commentCount.count > 0) {
        throw new Error('Cannot delete post with comments');
      }
    }
  }
}

beforeDataDelete

When: Immediately after beforeData, delete-specific
Context: Same as beforeData
What can be changed: Same as beforeData

afterData

When: After the resource is deleted from the database
Purpose: Clean up related data, log deletions

Context contains:

What can be changed:

Example:

hooks: {
  afterData: async ({ context, scopes }) => {
    if (context.method === 'delete') {
      // Log the deletion
      await scopes.audit_logs.create({
        action: 'delete',
        resource_type: 'posts',
        resource_id: context.id,
        user_id: context.auth?.userId,
        timestamp: new Date().toISOString()
      });
      
      // Clean up orphaned images
      await context.db('post_images')
        .where('post_id', context.id)
        .delete();
    }
  }
}

afterDataDelete

When: Immediately after afterData, delete-specific
Context: Same as afterData
What can be changed: Same as afterData

afterCommit

When: After the transaction is committed
Purpose: Trigger post-deletion side effects

Context: All accumulated context
What can be changed: Nothing - for side effects only

afterRollback

When: If an error occurs and transaction is rolled back
Context: All accumulated context plus error information
What can be changed: Nothing - for cleanup only

finish

When: Before returning the response (typically empty for DELETE)
Context: All accumulated context
What can be changed: Nothing - informational only

finishDelete

When: Immediately after finish, delete-specific
Context: Same as finish
What can be changed: Nothing - informational only

Special Hooks

enrichAttributes

The enrichAttributes hook is the correct way to add or modify attributes on records. This hook is called for ALL records - both main records and included/related records.

When: After records are fetched and before they are returned
Purpose: Add computed fields, transform attribute values, enhance record data

Context contains:

What can be changed:

Important:

Example:

// In global customize()
api.customize({
  hooks: {
    enrichAttributes: async ({ context }) => {
      // Add computed fields based on scope
      if (context.scopeName === 'posts') {
        context.attributes.wordCount = context.attributes.content?.split(' ').length || 0;
        context.attributes.readingTime = Math.ceil(context.attributes.wordCount / 200) + ' min';
        context.attributes.preview = context.attributes.content?.substring(0, 150) + '...';
      }
      
      if (context.scopeName === 'users') {
        // Hide sensitive data
        delete context.attributes.password;
        delete context.attributes.resetToken;
        
        // Add display name
        context.attributes.displayName = `${context.attributes.firstName} ${context.attributes.lastName}`;
      }
    }
  }
});

// In resource-specific extras
api.addScope('articles', {}, {
  hooks: {
    enrichAttributes: async ({ context }) => {
      // This only runs for articles
      context.attributes.isPublished = context.attributes.status === 'published';
      context.attributes.isNew = new Date() - new Date(context.attributes.created_at) < 7 * 24 * 60 * 60 * 1000;
      
      // Format dates for display
      context.attributes.formattedDate = new Date(context.attributes.created_at).toLocaleDateString();
    }
  }
});

knexQueryFiltering

The knexQueryFiltering hook is called during QUERY operations to apply filter conditions. This is a special hook that allows complex query modifications.

When: During dataQuery execution, before sorting and pagination
Purpose: Apply filters, add JOINs, modify query conditions

Context contains:

The REST API Knex Plugin registers three sub-hooks that run in sequence:

1. polymorphicFiltersHook

Purpose: Handles filtering on polymorphic relationships
What it does:

Example:

// This is handled automatically by the plugin
// When filtering: ?filters[commentable.title]=Hello
// It generates SQL like:
// LEFT JOIN posts ON (comments.commentable_type = 'posts' AND comments.commentable_id = posts.id)
// WHERE posts.title = 'Hello'

2. crossTableFiltersHook

Purpose: Handles filtering on cross-table fields
What it does:

Example:

// This is handled automatically by the plugin
// When filtering: ?filters[author.name]=John
// It generates SQL like:
// INNER JOIN users ON posts.author_id = users.id
// WHERE users.name = 'John'

3. basicFiltersHook

Purpose: Handles simple filters on the main table
What it does:

Custom Filter Hook Example:

hooks: {
  knexQueryFiltering: async ({ context }) => {
    if (context.knexQuery && context.knexQuery.filters) {
      const { query, filters, tableName } = context.knexQuery;
      
      // Add custom filter logic
      if (filters.special_filter) {
        query.where(function() {
          this.where(`${tableName}.status`, 'active')
              .orWhere(`${tableName}.featured`, true);
        });
      }
    }
  }
}

Hook Best Practices

1. Hook Order Matters

Hooks run in registration order. Consider dependencies between hooks:

hooks: {
  beforeData: [
    async ({ context }) => {
      // Validation runs first
      if (!context.inputRecord?.data?.attributes?.title) {
        throw new Error('Title is required');
      }
    },
    async ({ context }) => {
      // Enrichment runs second, after validation
      context.inputRecord.data.attributes.slug = context.inputRecord.data.attributes.title
        .toLowerCase()
        .replace(/\s+/g, '-');
    }
  ]
}

2. Use Proper Hook Placement

If using addHook directly (less common), you can control placement:

// Use afterPlugin to ensure your hook runs after the plugin's hooks
api.addHook('beforeData', 'myHook', { afterPlugin: 'rest-api-knex' }, handler);

3. Context Mutation Guidelines

4. Error Handling

Throwing an error in any hook will:

hooks: {
  beforeData: async ({ context }) => {
    if (context.inputRecord?.data?.attributes?.price < 0) {
      throw new RestApiValidationError('Price cannot be negative', {
        fields: ['data.attributes.price']
      });
    }
  }
}

5. Performance Considerations

6. Transaction Safety

For write operations:

7. Scope-Specific Hooks

Add hooks to specific scopes to avoid checking in every hook:

// Better: Add hooks in the scope's extras parameter
api.addScope('posts', {}, {
  hooks: {
    beforeData: async ({ context }) => {
      // This only runs for posts
    }
  }
});

// Less ideal: Check scopeName in global hooks
hooks: {
  beforeData: async ({ context }) => {
    if (context.scopeName === 'posts') {
      // ...
    }
  }
}

8. Hook Communication

Use context properties to communicate between hooks:

hooks: {
  beforeData: async ({ context }) => {
    context.customData = { processed: true };
  },
  
  afterData: async ({ context }) => {
    if (context.customData?.processed) {
      // React to first hook
    }
  }
}

## System-Wide Hooks

These hooks are managed by the hooked-api framework and are triggered during core API operations.

### plugin:installed

**When**: After a plugin is successfully installed  
**Purpose**: React to plugin installations, set up inter-plugin communication

**Context contains**:
- `pluginName` (string) - Name of the installed plugin
- `pluginOptions` (object) - Options passed to the plugin
- `plugin` (object) - The plugin object itself (informational only)

**What can be changed**: Nothing - this is an informational hook

**Example**:
```javascript
hooks: {
  'plugin:installed': async ({ context }) => {
    console.log(`Plugin ${context.pluginName} installed with options:`, context.pluginOptions);
  }
}

scope:added

When: After a scope is added to the API
Purpose: Initialize scope-specific settings, validate configurations, compile schemas

Context contains:

What can be changed:

Example:

hooks: {
  'scope:added': async ({ context }) => {
    // Add a default value to scope vars
    context.vars.defaultPageSize = 20;
    
    // Add a helper function
    context.helpers.formatDate = (date) => new Date(date).toISOString();
  }
}

method:api:added

When: After an API method is added
Purpose: Wrap or modify API method handlers

Context contains:

What can be changed:

Example:

hooks: {
  'method:api:added': async ({ context }) => {
    const originalHandler = context.handler;
    context.handler = async (params) => {
      console.log(`Calling ${context.methodName}`);
      const result = await originalHandler(params);
      console.log(`${context.methodName} completed`);
      return result;
    };
  }
}

method:scope:adding

When: Before adding a scope method
Purpose: Validate or modify scope methods before they’re registered

Context contains:

What can be changed:

method:scope:added

When: After a scope method is added
Purpose: React to scope method additions

Context contains:

What can be changed: Nothing - this is an informational hook

```


Back to Guide