Skip to main content

Overview

Bindings provide a powerful dependency injection system for handlers, allowing you to inject models, values, and custom dependencies into handler contexts.

BindingsHelpers

Helper methods available in the bindings factory function.

bind

Wraps a binding factory for type consistency.
bindings: (bind) => ({
  currentUser: bind((options) => ({
    resolve: async ({ request, fail }) => {
      const token = request?.headers.authorization;
      const user = await verifyToken(token);
      if (!user) throw fail('unauthorized', {});
      return user;
    },
  })),
})
factory
BindingFactory
required
A function that returns a binding definition
bindingName
string
Optional explicit name for the binding (for literal type preservation)
factory
BindingFactory
The wrapped binding factory with proper typing

model

Creates a model binding for automatic entity loading.
bindings: (bind) => ({
  user: bind.model(User, {
    from: 'params',
    fromKey: 'userId',
    primaryKey: 'id',
    async load({ id, fail }) {
      const user = await User.findById(id);
      if (!user) throw fail('notFound', { id });
      return user;
    },
  }),
})
model
ModelIdentifier
required
The model class or identifier to bind
options
BindingModelOptions | BindingModelOptions[]
Configuration for model loading
from
'params' | 'query' | 'headers' | 'body'
default:"params"
Source to extract the identifier from
fromKey
string
Key to extract from the source (defaults to model name)
primaryKey
string
default:"id"
Primary key field name
where
Record<string, unknown>
Additional where conditions to apply
transform
() => unknown
Function to transform the extracted value
load
(ctx: { [key]: unknown, fail }) => Promise<Model>
Custom loader function for fetching the model
notFound
(ctx: { [key]: unknown, fail }) => Promise<Model | undefined>
Handler for when the model is not found
factory
BindingFactory
A binding factory that loads and validates the model

value

Creates a static value binding.
bindings: (bind) => ({
  config: bind.value({
    apiUrl: 'https://api.example.com',
    timeout: 5000,
  }),
})
value
TValue
required
The value to inject into the handler context
factory
BindingFactory
A binding factory that returns the static value

BindingDefinition

The core binding definition interface.
interface BindingDefinition<
  TPayloadSchema,
  TResult,
  THandler,
  TResponseHandler,
  TMode
> {
  mode?: TMode;
  payload?: TPayloadSchema;
  inject?: (ctx: BindingInjectContext) => PromiseOr<boolean>;
  resolve: (ctx: BindingResolveContext) => Promise<TResult>;
}
mode
BindingMode
default:"toInject"
How the binding is injected into handlers
  • "toInject": Requires explicit opt-in via handler options
  • "alwaysInjected": Always injected into every handler
  • "variativeInject": Conditionally injected based on inject function
payload
Schema
Schema for validating the binding’s payload
inject
(ctx: BindingInjectContext) => Promise<boolean> | boolean
Function to determine if the binding should be injected (for variativeInject mode)
ctx.payload
InferBindingPayload
The validated payload
ctx.request
HandlerRequest | null
The current request object
ctx.bindingName
string
The name of this binding
ctx.handler
unknown
The handler context
resolve
(ctx: BindingResolveContext) => Promise<TResult>
required
Function that resolves the binding value
ctx.payload
InferBindingPayload
The validated payload
ctx.handler
unknown
The handler context
ctx.bindingName
string
The name of this binding
ctx.request
HandlerRequest | null
The current request object
ctx.fail
(name: string, input: any) => never
Function to throw formatted errors

BindingInstance

Marker type to prevent object destructuring in handler context.
type BindingInstance<T> = T & {
  [bindingInstanceSymbol]: never;
};
When a binding returns a BindingInstance, it will be injected under its binding name instead of being spread into the context.
// Without BindingInstance - fields are spread
const binding1 = () => ({
  resolve: async () => ({ name: 'John', age: 30 })
});
// Context: { name: 'John', age: 30 }

// With BindingInstance - kept under binding name
const binding2 = () => ({
  resolve: async () => ({
    name: 'John',
    age: 30,
    [bindingInstanceSymbol]: ''
  })
});
// Context: { user: { name: 'John', age: 30 } }

Usage Examples

Custom Authentication Binding

import { createOptions } from '@apisr/controller';
import { verifyJWT } from './auth';

const options = createOptions({
  bindings: (bind) => ({
    auth: bind((required = true) => ({
      mode: 'variativeInject',
      async inject({ request }) {
        // Only inject if authorization header exists
        return !!request?.headers.authorization;
      },
      async resolve({ request, fail }) {
        const token = request?.headers.authorization?.replace('Bearer ', '');
        if (!token && required) {
          throw fail('unauthorized', { message: 'Missing token' });
        }
        
        if (!token) return null;
        
        const user = await verifyJWT(token);
        if (!user && required) {
          throw fail('unauthorized', { message: 'Invalid token' });
        }
        
        return user;
      },
    })),
  }),
});

// Use in handler
const handler = createHandler(options);

const protectedRoute = handler(
  async ({ auth }) => {
    return { userId: auth.id };
  },
  { auth: true } // required auth
);

const optionalRoute = handler(
  async ({ auth }) => {
    return { userId: auth?.id };
  },
  { auth: false } // optional auth
);

Database Transaction Binding

const options = createOptions({
  bindings: (bind) => ({
    transaction: bind(() => ({
      mode: 'alwaysInjected',
      async resolve() {
        const trx = await db.transaction();
        
        return {
          db: trx,
          async commit() {
            await trx.commit();
          },
          async rollback() {
            await trx.rollback();
          },
          [bindingInstanceSymbol]: '',
        };
      },
    })),
  }),
});

Multi-Option Model Binding

const options = createOptions({
  bindings: (bind) => ({
    post: bind.model(Post, [
      {
        from: 'params',
        fromKey: 'postId',
        primaryKey: 'id',
      },
      {
        where: { published: true },
      },
      {
        async load({ id, fail }) {
          const post = await Post.findById(id);
          if (!post) throw fail('notFound', { id });
          return post;
        },
      },
    ]),
  }),
});

Build docs developers (and LLMs) love