-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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.
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.
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) => exampleObjectDefault: () => ({}) — no example, prepareListInput derives everything from index alone.
The four args:
-
query— parsed query-string asRecord<string, string>. Multi-value entries collapse to the first value. v1 / ALB prefermultiValueQueryStringParameters[k][0]when both forms are present. -
body— parsed request body,unknown. Only defined forPUT /-clone/PUT /-move, which have a body;nullonGET //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 intoevent.headers.cookiebefore 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.
- v1:
-
context— the LambdaContext: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.
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.
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 iseventPath.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.
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 witheventandcontextarguments. Callbacks written against the core handler still work (extra args ignored). -
mountPath— Lambda-specific. The bundlednode:httphandler 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.