Skip to content

Options

Eugene Lazutkin edited this page Apr 23, 2026 · 2 revisions

Options

createLambdaAdapter(adapter, options?) accepts an optional options object with the following shape:

interface LambdaAdapterOptions<TItem extends Record<string, unknown> = Record<string, unknown>> {
  policy?: Partial<RestPolicy>;
  sortableIndices?: Record<string, string>;
  keyFromPath?: (rawKey: string, adapter: Adapter<TItem>) => Record<string, unknown>;
  exampleFromContext?: (
    query: Record<string, string>,
    body: unknown,
    event: LambdaEvent,
    context: Context
  ) => Record<string, unknown>;
  maxBodyBytes?: number;
  mountPath?: string;
}

All options are optional. The factory is generic in TItem so that typed Adapter<Planet, PlanetKey> flows through to keyFromPath's second argument without casts.

policy

Partial overrides for the REST policy. Merged shallowly with defaultPolicy from dynamodb-toolkit/rest-core — only the fields you pass are touched.

createLambdaAdapter(adapter, {
  policy: {
    defaultLimit: 25,
    maxLimit: 500,
    maxOffset: 10_000,                 // DoS cap on `?offset=` (since toolkit 3.1.1)
    needTotal: false,
    envelope: {items: 'rows', total: 'count', offset: 'skip', limit: 'take', links: 'links'},
    metaPrefix: '$',
    methodPrefix: '!',
    statusCodes: {
      miss: 410,
      validation: 422,
      consistency: 409,
      throttle: 429,
      transient: 503,
      internal: 500
    }
  }
});

See the parent wiki's REST core page for the full RestPolicy reference.

sortableIndices

Maps the public sort-field name (?sort=field or ?sort=-field) to the GSI that provides that ordering. Without an entry, ?sort=… is ignored (no index set, no error).

createLambdaAdapter(adapter, {
  sortableIndices: {
    name:      'by-name-index',
    createdAt: 'by-created-index',
    mass:      'by-mass-index'
  }
});

?sort=-createdAt then resolves to {index: 'by-created-index', descending: true} and is passed to the Adapter's list call.

keyFromPath

Convert the URL :key path segment into a key object. Runs on every keyed route. Default:

(raw, adapter) => ({[adapter.keyFields[0].name]: raw})

Overrides are the idiomatic place for composite keys, type coercion, and key-format validation:

// Composite "partition:sort" keys
createLambdaAdapter(adapter, {
  keyFromPath: raw => {
    const [pk, sk] = raw.split(':');
    if (!pk || !sk) {
      throw Object.assign(new Error('Expected key of shape pk:sk'), {status: 400, code: 'BadKey'});
    }
    return {pk, sk};
  }
});

The adapter is passed through so you can read adapter.keyFields when writing generic callbacks. See Composite keys for more patterns.

exampleFromContext

Build the example object passed to Adapter.prepareListInput(example, index). Runs on the collection-level routes that call the Adapter's list machinery: GET /, DELETE /, PUT /-clone, PUT /-move.

Signature:

(query, body, event, context) => exampleObject

Default: () => ({}) — no example, prepareListInput derives everything from index alone.

The four args:

  • query — parsed query-string as Record<string, string>. Multi-value entries collapse to the first value. v1 / ALB prefer multiValueQueryStringParameters[k][0] when both forms are present.
  • body — parsed request body, unknown. Only defined for PUT /-clone / PUT /-move, which have a body; null on GET / / DELETE /.
  • event — the full Lambda event. v1 / v2 / ALB shapes all pass through; reach for whatever your trigger exposes:
    • v1: event.requestContext.identity (caller IAM / IP), event.requestContext.authorizer (custom authorizer claims), event.headers.
    • v2 / Function URL: event.requestContext.authorizer.lambda (Lambda authorizer claims), event.requestContext.authorizer.jwt.claims (JWT authorizer), event.headers. v2 cookies are already flattened into event.headers.cookie before this hook runs — read cookies uniformly regardless of trigger.
    • ALB: event.requestContext.elb (target-group ARN for multi-env routing), event.headers. No built-in authorizer claims; IAM / auth happens outside ALB.
  • context — the Lambda Context: context.awsRequestId, context.invokedFunctionArn, context.getRemainingTimeInMillis(), etc. Useful for correlation IDs and deadline-aware operations.
// Per-tenant scoping — tenant from a JWT claim on v2, custom authorizer on v1.
createLambdaAdapter(adapter, {
  exampleFromContext: (query, _body, event, _context) => {
    const tenantId =
      event.requestContext?.authorizer?.jwt?.claims?.tenant_id     // v2 JWT
      ?? event.requestContext?.authorizer?.lambda?.tenantId        // v2 Lambda authorizer
      ?? event.requestContext?.authorizer?.tenantId                // v1 custom authorizer
      ?? event.headers?.['x-tenant-id'];                           // dev / direct
    if (!tenantId) {
      throw Object.assign(new Error('tenantId required'), {status: 401, code: 'MissingTenant'});
    }
    return {tenantId, status: query.status || 'active'};
  }
});

The example flows into the Adapter's prepareListInput(example, index) hook, which shapes KeyConditionExpression / FilterExpression against the GSI in use. See the parent wiki's Adapter > prepareListInput for the hook contract.

maxBodyBytes

Byte cap for request bodies. Enforced on every body-reading route. The adapter decodes the body (base64 if event.isBase64Encoded is true) and rejects 413 PayloadTooLarge before JSON.parse when the decoded byte length exceeds the cap.

Default: 1048576 (1 MiB), matching dynamodb-toolkit/handler + the koa / express / fetch adapters.

// Low-power device endpoint — cap at 64 KiB
createLambdaAdapter(adapter, {maxBodyBytes: 64 * 1024});

// Dedicated bulk-load endpoint — allow larger bodies (subject to Lambda's own cap)
createLambdaAdapter(bulkAdapter, {maxBodyBytes: 5 * 1024 * 1024});

Lambda also enforces platform-level caps, and they're different per trigger:

Trigger Platform cap
API Gateway REST (v1) 10 MB (sync), 6 MB via SDK
API Gateway HTTP (v2) 6 MB
Lambda Function URL 6 MB
ALB 1 MB (default), configurable up to tens of MB

maxBodyBytes is the tenant-level cap — the value you want enforced at the adapter layer before JSON.parse runs. Keep it ≤ the trigger's platform cap; the adapter cap fires first and returns a clean JSON 413 instead of AWS's raw platform error.

Over-cap requests get 413 PayloadTooLarge with {code: 'PayloadTooLarge'}. Malformed JSON gives 400 BadJsonBody. See Body reading.

mountPath

Path prefix the adapter owns. Stripped from the incoming path before route matching.

Default: unset — the adapter sees the full event path and treats it as rooted at /.

// Function URL — Lambda owns the whole URL.
const handler = createLambdaAdapter(planets);

// Mounted under /planets — GET /planets/earth → adapter sees GET /earth.
const handler = createLambdaAdapter(planets, {mountPath: '/planets'});

Match rules:

  • Exact match (eventPath === mountPath) → route is /.
  • Prefix match (eventPath.startsWith(mountPath + '/')) → route is eventPath.slice(mountPath.length).
  • Anything else → empty 404.

Matching is case-sensitive. No trailing-slash normalization beyond what the trigger does to the event path.

Stage prefixes are the trigger's job. API Gateway stage variables (/prod, /dev) are stripped before the Lambda sees the event — event.path on v1 is already /planets/earth, not /prod/planets/earth. Set mountPath to the adapter's logical root (/planets), not the full external URL.

ALB doesn't strip anything — the path on the event is whatever the client sent. Keep mountPath aligned with the listener-rule path-pattern if the rule routes a subtree.

Interaction with parent HandlerOptions

The options surface is a near-superset of dynamodb-toolkit/handler's HandlerOptions:

  • policy, sortableIndices, keyFromPath, maxBodyBytes — identical shape and semantics.
  • exampleFromContext — extended from the core's (query, body) signature with event and context arguments. Callbacks written against the core handler still work (extra args ignored).
  • mountPath — Lambda-specific. The bundled node:http handler doesn't take a mount because it assumes rooted paths.

No onMiss hook (unlike the fetch adapter) — Lambda handlers are always terminal. Custom miss-body shapes go through policy.errorBody + policy.statusCodes.miss like any other error.

Clone this wiki locally