-
Notifications
You must be signed in to change notification settings - Fork 0
Framework adapters
The toolkit ships its REST route pack for four HTTP surfaces as subpath exports. Every adapter is a thin port over the same internal engine — identical routes, envelopes, status codes, and error mapping — translated to its framework's request/response shape. Frameworks are duck-typed at runtime, so the core keeps zero runtime dependencies.
| Sub-export | Factory | Serves | Details |
|---|---|---|---|
dynamodb-toolkit/express |
createExpressAdapter |
Express 4.x / 5.x middleware | Express adapter |
dynamodb-toolkit/koa |
createKoaAdapter |
Koa 2.x / 3.x middleware | Koa adapter |
dynamodb-toolkit/fetch |
createFetchAdapter |
(Request) => Promise<Response> — Cloudflare Workers, Deno Deploy, Bun.serve, Hono, Node |
Fetch adapter |
dynamodb-toolkit/lambda |
createLambdaAdapter |
AWS Lambda — API Gateway REST / HTTP, Function URL, ALB | Lambda adapter |
All four take the same two arguments: the Adapter instance doing the DynamoDB work, and an options bag documented below. The bundled node:http handler (HTTP handler) speaks the same wire contract; the former standalone dynamodb-toolkit-{koa,express,fetch,lambda} packages are superseded by these subpaths.
Routes are rooted at the adapter's mount point. All requests and responses use JSON; the envelope keys, status codes, and the - method-prefix are configurable via options.policy (see REST core).
| Method | Path | Adapter call | Response |
|---|---|---|---|
| GET | / |
getList(opts, example, index) |
200 — list envelope |
| GET | /?cursor |
getPage(opts, example, index) |
200 {data, limit, cursor?} — cursor paging |
| POST | / |
post(body) |
204 — no body |
| DELETE | / |
deleteListByParams(params, massOpts) |
200 {processed, skipped?, failed?, …} |
| GET | /-by-keys |
getByKeys(keys, fields, opts) |
200 — array of items |
| DELETE | /-by-keys |
deleteByKeys(keys) |
200 {processed: N} |
| PUT | /-load |
putItems(items) |
200 {processed: N} |
| PUT | /-clone |
cloneListByParams(params, mapFn, mo) |
200 {processed, skipped?, failed?, …} |
| PUT | /-move |
moveListByParams(params, mapFn, mo) |
200 {processed, skipped?, failed?, …} |
| PUT | /-clone-by-keys |
cloneByKeys(keys, mapFn) |
200 {processed: N} |
| PUT | /-move-by-keys |
moveByKeys(keys, mapFn) |
200 {processed: N} |
| GET | /:key |
getByKey(key, fields, opts) |
200 item, or 404 on miss |
| PUT | /:key |
put({...body, ...key}, {force}) |
204 — no body |
| PATCH | /:key |
patch(key, patch, patchOptions) |
204 — no body |
| DELETE | /:key |
delete(key) |
204 — no body |
| PUT | /:key/-clone |
clone(key, mapFn, {force}) |
204 / 404
|
| PUT | /:key/-move |
move(key, mapFn, {force}) |
204 / 404
|
Notes shared by all four adapters:
-
HEAD /:keyauto-promotes toGET /:keyviamatchRoute. - A known route shape with an unsupported method returns
405 Method Not Allowedwith a JSON error body. -
Miss handling differs by adapter: Express and Koa hand unknown route shapes back to the middleware chain (
next()); the Fetch adapter answers an empty404or runs itsonMisshook; the Lambda adapter answers an empty404. - The
-by-namesroute spellings and thenamesquery param remain as legacy aliases of-by-keys/keys. -
DELETE /-by-keys,PUT /-clone-by-keys, andPUT /-move-by-keysaccept the key list either as?keys=a,b,cor as a JSON array body when the query is absent. The-clone/-movevariants also accept an object body as an overlay merged into every processed item; sending both requires?keys=…in the query plus the overlay object in the body. -
Unscoped-delete guard:
DELETE /with no filter / search / example scope is400 UnscopedMassDeleteunless?confirm=true(disable viapolicy.confirmMassDelete: false). - The mass routes accept
?max-items=N/?resume=<token>and answer with the full mass-result body — optional keys (skipped/failed/conflicts/cursor) appear only when non-empty. -
GET /also accepts?cursor[=token](cursor paging — no offset/total/links; keep paging whilecursoris present) and?format=jsonl(NDJSON, one item per line; the next-page token rides thex-cursorheader in cursor mode). - Write routes (
POST /,PUT /:key,PATCH /:key) reject non-object bodies with400 BadBody; malformed JSON is400 BadJsonBody; over-cap bodies are413 PayloadTooLarge. - Query parameters for list routes (paging, projection, filtering, sorting) are parsed by REST core — see that page for the accepted grammar and DoS caps.
Every factory accepts (adapter, options?) with these options (each adapter's .d.ts carries the full JSDoc):
-
policy— partial overrides for the REST policy (envelope keys, status codes, limits,metaPrefix,methodPrefix), merged with the default. Reference: REST core. -
sortableIndices— map from public sort-field name to the index that provides that ordering;?sort=nameresolves to{index: sortableIndices.name, descending: false}and is passed to the Adapter's list call. -
keyFromPath(rawKey, adapter)— convert the URL:keysegment into a key object. Runs on every keyed route. Default:(raw, adp) => ({[adp.keyFields[0].name]: raw}). See Composite keys from the URL below. -
exampleFromContext(context)— build theexampleobject passed toAdapter.prepareListInput. Runs onGET /,DELETE /,PUT /-clone, andPUT /-move. The context is an options bag{query, body, adapter, framework, ...}whereframeworkis the discriminator ('express' | 'koa' | 'fetch' | 'lambda') and the remaining fields are framework-specific (reqon Express,ctxon Koa,requeston Fetch,event+contexton Lambda) — one tenant-scoping callback can serve all four adapters by branching onframework. See Per-tenant filtering below. -
maxBodyBytes— byte cap for request bodies, default1048576(1 MiB). Enforcement details differ per adapter (pre-parsed bodies on Express/Koa bypass it; Fetch enforces pre-stream and mid-stream; Lambda enforces on decoded bytes) — see the per-adapter pages.
Adapter-specific options: mountPath (Fetch, Lambda) strips a path prefix before route matching; onMiss (Fetch only) composes the adapter into a parent router. Documented on Fetch adapter and Lambda adapter.
The default keyFromPath covers single-field keys. For composite keys, parse the :key segment yourself — the callback is framework-independent, so these patterns work with every adapter:
// Two-part keys with a URL-safe delimiter
keyFromPath: raw => {
const [customerId, orderId] = raw.split(':');
if (!customerId || !orderId) {
throw Object.assign(new Error('Key must be "customerId:orderId"'), {status: 400, code: 'BadKey'});
}
return {customerId, orderId};
}// Numeric partition keys with validation
keyFromPath: (raw, a) => {
const id = Number(raw);
if (!Number.isFinite(id) || !Number.isInteger(id) || id < 0) {
throw Object.assign(new Error('Numeric positive integer key expected'), {status: 400});
}
return {[a.keyFields[0].name]: id};
}Thrown errors with a status field map straight onto the wire, so key-format validation lives naturally inside the callback. Pick a delimiter that won't appear in key values: : and | are URL-safe without encoding; / interferes with path segments; , interferes with ?names= parsing.
PUT /:key merges the URL key into the body, so keyFromPath must return all key fields. /-by-names?names=… runs each comma-separated entry through the same callback.
Constraining list operations to the current tenant is what exampleFromContext is for: it feeds the example argument of Adapter.prepareListInput(example, index), which the Adapter's consumer-supplied hook turns into a key condition.
GET /?status=active
-> exampleFromContext({query, body: null, adapter, framework: 'express', req})
-> returns {tenantId: 'acme', status: 'active'}
-> Adapter.getList(opts, example, index)
-> Adapter.prepareListInput(example, index) <- consumer-supplied hook
-> {IndexName: 'by-tenant', KeyConditionExpression: ..., ExpressionAttributeValues: {':t': 'acme'}}
-> DynamoDB Query
Auth-derived tenant on Express (the other adapters differ only in the context fields):
app.use(auth); // sets req.user.tenantId
app.use(
'/items',
createExpressAdapter(items, {
exampleFromContext: ({query, framework, req}) => ({
tenantId: req.user.tenantId,
status: query.status || 'active'
})
})
);The same callback shared across adapters branches on the discriminator:
const exampleFromContext = ctx => {
switch (ctx.framework) {
case 'express':
return {tenantId: ctx.req.user.tenantId};
case 'koa':
return {tenantId: ctx.ctx.state.user.tenantId};
case 'lambda':
return {tenantId: ctx.event.requestContext.authorizer?.lambda?.tenantId};
case 'fetch':
return {tenantId: ctx.request.headers.get('x-tenant-id')};
}
};body is the parsed request body on PUT /-clone / PUT /-move and null on GET / / DELETE /. Use exampleFromContext for key-condition scoping (which GSI, which partition); post-query field predicates belong to the filter grammar parsed by REST core — the two compose.
- Express adapter · Koa adapter · Fetch adapter · Lambda adapter — per-adapter setup, body handling, error handling, and quirks.
-
Plain Express routes — the no-adapter path: hand-written routes calling
Adaptermethods directly. -
HTTP handler — the bundled
node:httpflavor of the same route pack. - REST core — parsers, envelope builders, and the policy every adapter shares.
Start here
- Getting started
- Concepts
- Key and field design
- Compatibility
- Migration: v2 to v3
- SDK v2 to v3 cheat sheet
Guides
- Hierarchical data walkthrough
- Key expression patterns
- Multi-type tables
- Pagination
- Mass operation semantics
- URL schema design
Adapter
- Adapter
- Constructor options
- CRUD methods
- Mass methods
- Batch builders
- Hooks
- Raw marker
- Indirect indices
- Transaction auto-upgrade
Expression builders
Batch / transactions / mass / paths
REST surface
Framework adapters
Recipes
- Recipes index
- List records of a tier
- Per-tier sparse GSI markers
- Tier within a partition
- Reservation with auto-release
- Keys-only GSI, runtime projection
- Cascade subtree operations
- Querying subtrees with buildKey
- Filter URL grammar
- Text search
- Provisioning workflow
- Resumable mass operations
History