API Reference
Documentation
- 📚 Full Documentation - Complete guide with examples
- 🔧 API Reference - You’re here!
- ⚡ Cheatsheet - Quick recipes and code snippets
Public API Surface
The API instance exposes these public properties and methods:
await api.use(plugin, options)
- Install plugins with optional configurationawait api.customize(config)
- Add hooks, methods, vars, and helpers after initializationawait api.addScope(name, options, extras)
- Add scopes with configuration and optional customizationsapi.setScopeAlias(aliasName, addScopeAlias)
- Create aliases for the scopes property and addScope methodapi.scopes
- Access to defined scopes (e.g.,api.scopes.users.get()
)api.scopes.[scopeName].vars
- Direct access to scope variables (falls back to global vars)api.scopes.[scopeName].helpers
- Direct access to scope helpers (falls back to global helpers)
api.[aliasName]
- If setScopeAlias was called (e.g.,api.tables
for database APIs)api.[addScopeAlias]
- If setScopeAlias was called with second parameter (e.g.,api.addTable
)api.[methodName]()
- Direct calls to defined API methodsapi.vars
- Direct proxy access to global variablesapi.helpers
- Direct proxy access to global helpersapi.options
- Read-only access to the API configuration (includes name, version, and merged logging config)
Static Methods
Api.registry.get(name, version)
- Get a registered API instanceApi.registry.list()
- List all registered APIs and their versionsApi.registry.has(name, version)
- Check if an API is registeredApi.registry.versions(name)
- Get all versions of a specific API
Handler Context Reference
Global API Methods
When you define an API method (via apiMethods
in customize or via plugin), the handler receives:
// Handler signature for global API methods:
async ({
params, // Parameters passed to the method call
context, // Mutable object for passing data between hooks
vars, // Variables proxy
helpers, // Helpers proxy
scope, // null (no current scope for global methods)
scopes, // Access to all scopes (api.scopes)
runHooks, // Function to run hooks: runHooks(hookName)
log, // Logger instance for this context
name, // The method name being called
apiOptions, // Frozen API configuration {name, version, ...}
pluginOptions, // Frozen plugin configurations {pluginName: options, ...}
// If setScopeAlias was called:
[aliasName] // Same as 'scopes' but with custom name (e.g., 'tables')
}) => {
// Implementation
await runHooks('beforeProcess');
const result = await doSomething(params);
context.result = result;
await runHooks('afterProcess');
return context.result;
}
Scope Methods
When you define a scope method (via scopeMethods
in customize or via plugin), the handler receives:
// Handler signature for scope methods:
async ({
params, // Parameters passed to the method call
context, // Mutable object for passing data between hooks
vars, // Variables proxy (merged: global + scope vars)
helpers, // Helpers proxy (merged: global + scope helpers)
scope, // Current scope object (e.g., api.scopes.users)
scopes, // All scopes proxy (api.scopes)
runHooks, // Function to run hooks: runHooks(hookName)
log, // Logger instance for this context
name, // The method name being called
apiOptions, // Frozen API configuration {name, version, ...}
pluginOptions, // Frozen plugin configurations {pluginName: options, ...}
scopeOptions, // Frozen scope-specific options (passed to addScope)
scopeName, // Current scope name as string (e.g., 'users')
// If setScopeAlias was called:
[aliasName] // Same as 'scopes' but with custom name (e.g., 'tables')
}) => {
// Implementation
console.log(`Called ${name} on scope ${scopeName}`);
// Can call other methods on current scope directly:
await scope.validate(params);
// Or access other scopes:
const relatedData = await scopes.related.get({ id: params.relatedId });
return processData(params, scopeOptions);
}
Example: Using Scope Aliases
api.setScopeAlias('tables');
// Now in handlers:
async ({ params, scope, scopes, tables, scopeName }) => {
// 'tables' is the same as 'scopes'
// 'scope' is the current scope object
// Clean, domain-specific syntax:
await scope.validate(params); // Validate current table
await tables.orders.get({ userId: params.id }); // Access orders table
}
Hook Handlers
Hook handlers receive a different parameter name for user data:
// Hook handler signature (when added via plugin or customize):
async ({
methodParams, // The params passed to the original method call
context, // The context object from the method (mutable, shared between hooks)
vars, // Variables (scope-aware if hook run with scope)
helpers, // Helpers (scope-aware if hook run with scope)
scope, // Current scope object if hook run in scope context, null otherwise
scopes, // All scopes proxy (api.scopes)
runHooks, // Function to run hooks (careful of recursion!)
log, // Logger instance for this context
name, // Hook name (e.g., 'beforeFetch', 'afterFetch')
apiOptions, // Frozen API configuration
pluginOptions, // Frozen plugin configurations
scopeOptions, // Frozen scope options (only if hook run with scope)
scopeName, // Scope name or null
// If setScopeAlias was called:
[aliasName] // Same as 'scopes' but with custom name
}) => {
// Hook handler implementation
console.log(`Hook ${name} called with params:`, methodParams);
// Modify context to share data with other hooks or the method
context.processedBy = context.processedBy || [];
context.processedBy.push(name);
// Return false to stop the hook chain
if (context.skipRemaining) {
return false;
}
}
Important Notes on Hooks:
- Hooks receive
methodParams
instead ofparams
to distinguish from method handlers - The
context
object is shared between all hooks and the method - Returning
false
from a hook stops the execution of remaining hooks in the chain - Hooks can be global (run for all scopes) or scope-specific
Plugin-Level Hooks
Plugins can create their own hookable operations using the runHooks
function provided in the install context. This extends the hook concept beyond method lifecycle to any plugin operation.
Naming Convention
- Method lifecycle hooks:
beforeSave
,afterFetch
,beforeDelete
, etc. - Plugin operation hooks: Use plugin:operation format like
http:request
,graphql:query
,websocket:message
Example: HTTP Plugin with Hookable Request Handling
const HttpPlugin = {
name: 'http',
install({ runHooks, api, log }) {
api.http = {
async handleRequest(req, res) {
// Create context for the operation
const context = {
req,
res,
handled: false,
auth: { userId: null, claims: null }
};
// Run hooks for HTTP request processing
context.url = req.url;
context.method = req.method;
context.headers = req.headers;
const shouldContinue = await runHooks('http:request', context);
// Check if a hook handled the request
if (!shouldContinue || context.handled) {
return; // Request was intercepted
}
// Continue with normal HTTP processing
log.info(`Processing ${req.method} ${req.url}`);
// ... rest of implementation
}
};
}
};
// Another plugin can hook into HTTP requests
const AuthPlugin = {
name: 'auth',
install({ addHook }) {
addHook('http:request', 'authenticate', {}, async ({ context }) => {
const { headers, url } = context;
// Handle auth endpoints
if (url.startsWith('/api/auth/')) {
// Process authentication
context.res.end(JSON.stringify({ token: 'new-token' }));
context.handled = true;
return false; // Stop processing
}
// Extract auth from headers
if (headers.authorization) {
context.auth.userId = extractUserId(headers.authorization);
context.auth.claims = extractClaims(headers.authorization);
}
return true; // Continue to next hook/handler
});
}
};
This pattern allows plugins to create extensible operations that other plugins can participate in, just like methods have lifecycle hooks.
Plugin System
Plugin Structure
const myPlugin = {
name: 'myPlugin', // Required: unique plugin name
dependencies: ['otherPlugin'], // Optional: array of required plugin names
install: (installContext) => { // Required: installation function
// Plugin setup code
}
};
Plugin Install Context
The install function receives a context object with these properties:
install: ({
// Setup methods
addApiMethod, // Function to add global API methods
addScopeMethod, // Function to define scope methods
addScope, // Function to add scopes
setScopeAlias, // Function to create scope aliases
// Hook management
addHook, // Special function that auto-injects plugin name:
// addHook(hookName, functionName, hookOptions, handler)
runHooks, // Run hooks from plugin context:
// runHooks(hookName, context)
// Event management
on, // Register event listeners:
// on(eventName, listenerName, handler)
// Data access
vars, // Variables proxy (mutable)
helpers, // Helpers proxy (mutable)
scopes, // Access to all scopes
// Logging
log, // Logger instance for this plugin context
// Plugin metadata
name, // Plugin name (same as plugin.name)
apiOptions, // Frozen API configuration
pluginOptions, // Frozen plugin configurations
context, // Empty context object for plugin use
// API instance
api // The API instance itself (to define more properties)
}) => {
// Example usage:
// Add a global method
addApiMethod('getData', async ({ params, vars, helpers }) => {
return await helpers.fetch(params.url);
});
// Add a scope method
addScopeMethod('validate', async ({ params, scopeOptions }) => {
return validateAgainstSchema(params, scopeOptions.schema);
});
// Add a hook
addHook('beforeFetch', 'addAuth', {}, async ({ context, vars }) => {
context.headers = { ...context.headers, Authorization: vars.apiKey };
});
// Create a hookable operation in your plugin
api.handleRequest = async (req, res) => {
const requestContext = { req, res, handled: false };
// Run hooks for this plugin operation
requestContext.url = req.url;
requestContext.method = req.method;
const shouldContinue = await runHooks('http:request', requestContext);
if (!shouldContinue || requestContext.handled) {
return; // Request was intercepted by a hook
}
// Continue with normal processing
};
// Set variables
vars.apiKey = 'default-key';
// Add helpers
helpers.fetch = async (url) => {
// Custom fetch implementation
};
}
Using Plugins
// Install a plugin
await api.use(myPlugin);
// Install with options
await api.use(myPlugin, {
apiKey: 'custom-key',
endpoint: 'https://api.example.com'
});
// Options are available in handlers via pluginOptions
async ({ pluginOptions }) => {
const options = pluginOptions.myPlugin; // { apiKey: 'custom-key', ... }
}
Event System
Overview
The event system provides lifecycle notifications separate from the hook system. While hooks intercept and can modify behavior, events are fire-and-forget notifications about system changes.
Event Registration
Plugins can register event listeners using the on
method in their install context:
const myPlugin = {
name: 'my-plugin',
install({ on }) {
// on(eventName, listenerName, handler)
on('scope:added', 'handleNewScope', async (eventContext) => {
console.log(`Scope ${eventContext.eventData.scopeName} was added`);
});
}
};
Event Handler Context
Event handlers receive a context object with:
{
eventName: string, // The event that was triggered
eventData: Object, // Event-specific data (see events below)
api: { // Read-only API access
vars: Proxy, // API variables (proxy)
helpers: Proxy, // API helpers (proxy)
scopes: Proxy, // All scopes
options: Object, // Frozen API options
pluginOptions: Object // Frozen plugin options
},
log: Logger // Context-specific logger
}
Available Events
scope:added
Emitted after a scope is successfully added to the API.
eventData: {
scopeName: string, // Name of the added scope
scopeOptions: Object, // Options passed to addScope
scopeExtras: Object // Extras (hooks, methods, etc.) passed to addScope
}
method:api:added
Emitted after an API method is added.
eventData: {
methodName: string, // Name of the added method
handler: Function // The method handler function
}
method:scope:added
Emitted after a scope method template is added.
eventData: {
methodName: string, // Name of the added method
handler: Function // The method handler function
}
plugin:installed
Emitted after a plugin is successfully installed.
eventData: {
pluginName: string, // Name of the installed plugin
pluginOptions: Object, // Options passed to await api.use()
plugin: Object // The plugin object itself
}
Event System Internals
The event system uses three private methods on the Api instance:
_on(eventName, pluginName, listenerName, handler)
Registers an event listener. Called automatically by the plugin install context’s on
method.
_emit(eventName, eventData)
Emits an event to all registered listeners. Called internally when system changes occur.
_removeListener(eventName, listenerName)
Removes a specific event listener. Returns true if the listener was found and removed.
Error Handling
Event handler errors are isolated - they are logged but don’t propagate or stop execution:
on('scope:added', 'mightFail', async ({ eventData }) => {
throw new Error('This error is logged but does not stop scope creation');
});
Best Practices
- Use unique listener names - Makes debugging easier and allows specific removal
- Keep handlers lightweight - Events run synchronously and can impact performance
- Don’t rely on event ordering - While listeners execute in registration order, this shouldn’t be depended upon
- Use events for side effects only - Events cannot cancel operations or modify behavior
- Access API state read-only - While
api.vars
is technically mutable, avoid modifications that affect core behavior
Example: Comprehensive Event Plugin
const EventMonitorPlugin = {
name: 'event-monitor',
install({ on, addApiMethod, vars }) {
// Initialize tracking
vars.eventLog = [];
// Register for all events
on('scope:added', 'logScope', ({ eventData, api }) => {
api.vars.eventLog.push({
type: 'scope',
name: eventData.scopeName,
timestamp: Date.now()
});
});
on('method:api:added', 'logApiMethod', ({ eventData, api }) => {
api.vars.eventLog.push({
type: 'api-method',
name: eventData.methodName,
timestamp: Date.now()
});
});
on('plugin:installed', 'logPlugin', ({ eventData, api }) => {
if (eventData.pluginName !== 'event-monitor') {
api.vars.eventLog.push({
type: 'plugin',
name: eventData.pluginName,
timestamp: Date.now()
});
}
});
// Expose the event log
addApiMethod('getEventLog', async ({ vars }) => {
return vars.eventLog;
});
}
};
Testing
Registry Management
The library maintains a global registry of API instances by name and version. For testing, you can reset this registry:
import { resetGlobalRegistryForTesting } from './index.js';
// In your test setup
beforeEach(() => {
resetGlobalRegistryForTesting();
});
// Now you can create APIs with the same name/version in each test
test('my test', () => {
const api = new Api({ name: 'test-api', version: '1.0.0' });
// ... test code
});
Testing Best Practices
- Reset the registry between tests to avoid conflicts
- Use unique API names per test if running tests in parallel
- Mock external dependencies in your plugins
- Test hooks independently by creating minimal APIs
// Example: Testing a plugin
import { Api } from './index.js';
test('myPlugin adds expected functionality', async () => {
const api = new Api({ name: 'test', version: '1.0.0' });
const myPlugin = {
name: 'test-plugin',
install: ({ addApiMethod, vars }) => {
vars.testValue = 'plugin-loaded';
addApiMethod('getValue', async ({ vars }) => vars.testValue);
}
};
await api.use(myPlugin);
const result = await api.getValue();
expect(result).toBe('plugin-loaded');
});
Error Handling
The library exports several error classes that it throws in different scenarios. You can catch these for specific error handling:
Error Classes
All errors extend from HookedApiError
which includes a code
property for programmatic error handling.
ValidationError
Thrown when validation fails (invalid method names, scope names, parameters, etc.)
try {
api.addScope('123-invalid-name', {});
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.code); // 'VALIDATION_ERROR'
console.log(error.field); // 'name'
console.log(error.value); // '123-invalid-name'
console.log(error.validValues); // 'valid JavaScript identifier'
}
}
Properties:
field
- The field that failed validationvalue
- The invalid value providedvalidValues
- Description of what’s expected
PluginError
Thrown when plugin operations fail (installation, dependencies, naming conflicts)
try {
await api.use({ name: 'api' }); // Reserved name
} catch (error) {
if (error instanceof PluginError) {
console.log(error.code); // 'PLUGIN_ERROR'
console.log(error.pluginName); // 'api'
console.log(error.installedPlugins); // ['other-plugin', ...]
}
}
Properties:
pluginName
- The plugin that caused the errorinstalledPlugins
- Array of currently installed plugins
ConfigurationError
Thrown when API configuration is invalid (missing name, invalid version, etc.)
try {
const api = new Api({ version: 'invalid' });
} catch (error) {
if (error instanceof ConfigurationError) {
console.log(error.code); // 'CONFIGURATION_ERROR'
console.log(error.received); // 'invalid'
console.log(error.expected); // 'semver format (e.g., 1.0.0)'
console.log(error.example); // "{ version: '1.0.0' }"
}
}
Properties:
received
- What was providedexpected
- What was expectedexample
- Example of correct usage
ScopeError
Thrown when scope operations fail (scope not found, duplicate scope names)
try {
await api.scopes.nonexistent.method();
} catch (error) {
if (error instanceof ScopeError) {
console.log(error.code); // 'SCOPE_ERROR'
console.log(error.scopeName); // 'nonexistent'
console.log(error.availableScopes); // ['users', 'posts', ...]
}
}
Properties:
scopeName
- The scope that caused the erroravailableScopes
- Array of available scope names
MethodError
Thrown when method operations fail (conflicts, invalid calls)
try {
api.scopes.users(); // Direct scope call
} catch (error) {
if (error instanceof MethodError) {
console.log(error.code); // 'METHOD_ERROR'
console.log(error.methodName); // 'users'
console.log(error.suggestion); // 'api.scopes.users.methodName()'
}
}
Properties:
methodName
- The method that caused the errorsuggestion
- Suggested correct usage
Importing Error Classes and Constants
import {
Api,
LogLevel,
HookedApiError,
ValidationError,
PluginError,
ConfigurationError,
ScopeError,
MethodError
} from './index.js';
// Catch all library errors
try {
// ... api operations
} catch (error) {
if (error instanceof HookedApiError) {
console.log('Library error:', error.code, error.message);
}
}