-
Notifications
You must be signed in to change notification settings - Fork 0
HTTP handler
See also (AWS JS SDK v3): any of the SDK Command pages this handler wraps (see linked sub-pages). Vocabulary: Concepts.
Using a framework? For Koa, Express, Fetch-API runtimes (Cloudflare Workers / Deno / Bun / Hono), or AWS Lambda, use the matching adapter package instead of this
node:httphandler. The wire contract (routes, query params, envelope, errors) is identical. See the Framework adapters list on the home page.
dynamodb-toolkit/handler ships a (req, res) => function that wires the REST core parsers/builders to a node:http server. Drop it into createServer and you have a working REST API in front of an Adapter. Design-level companions: URL schema design (this route pack and two alternatives), Pagination (offset vs cursor on the wire), Mass operation semantics (the mass-route contract).
import {createServer} from 'node:http';
import {createHandler} from 'dynamodb-toolkit/handler';
const handler = createHandler(adapter, {
sortableIndices: {name: '-t-name-index'}
});
createServer(handler).listen(3000);| Method | URL | Adapter call | Response |
|---|---|---|---|
GET |
/ |
getList + envelope |
200 with {data, offset, limit, total?, links?}
|
GET |
/?cursor[=token] |
getPage + envelope |
200 with {data, limit, cursor?} — cursor paging; bare ?cursor = first page |
GET |
/?format=jsonl |
getList / getPage
|
200 NDJSON — one item per line, no envelope; in cursor mode the next-page token rides the x-cursor header |
POST |
/ |
post |
204; 409 if key exists |
DELETE |
/?<op>-<field>=… / /?search=…
|
deleteListByParams |
200 with {processed, skipped?, failed?, conflicts?, cursor?}
|
GET |
/-by-keys?keys=…&fields=… |
getByKeys |
200 with plain array (missing dropped) |
DELETE |
/-by-keys?keys=… |
deleteByKeys |
200 with {processed}
|
PUT |
/-load |
putItems (body: array) |
200 with {processed}
|
PUT |
/-clone / /-move
|
cloneListByParams / moveListByParams (body: overlay) |
200 with {processed, skipped?, failed?, conflicts?, cursor?}
|
PUT |
/-clone-by-keys?keys=… / /-move-by-keys?keys=…
|
cloneByKeys / moveByKeys
|
200 with {processed}
|
GET |
/:key |
getByKey |
200 with item; 404 on miss |
PUT |
/:key?force=true |
put |
204; 409 without force on missing |
PATCH |
/:key |
patch (body: {field, _delete, _separator, _arrayOps}) |
204 |
DELETE |
/:key |
delete |
204 (idempotent) |
PUT |
/:key/-clone / /:key/-move
|
single-item clone / move
|
204; 404 on miss |
/:key URL-decodes the segment. The default keyFromPath maps it to {[adapter.keyFields[0]]: rawKey} — pass a custom keyFromPath for composite keys. The -by-names route spellings and the names query param remain as legacy aliases of -by-keys / keys.
Unscoped-delete guard. DELETE / with no scoping — no filter, no search, and an empty exampleFromContext — is rejected with 400 UnscopedMassDelete unless the request carries ?confirm=true. Disable via policy.confirmMassDelete: false.
Resumable mass ops on the wire. The mass routes (DELETE /, PUT /-clone, PUT /-move) accept ?max-items=N (soft per-request cap, page-boundary enforced) and ?resume=<token> (the cursor from a prior response). The optional mass-result keys (skipped / failed / conflicts / cursor) appear only when non-empty — a clean run still answers {processed: N}.
-
?fields=name,climate— projection (passed toaddProjection) -
?<op>-<field>=<value>— structured filter clause. Op prefixes:eq,ne,lt,le,gt,ge,in,btw,beg,ct,ex,nx. Multiple clauses AND together. Field + op must be in the adapter'sfilterableallowlist; unlisted rejections produceBadFilterField/BadFilterOp. Multi-value ops (in,btw) use first-character delimiter with,fallback:?in-status=available,rentedor?btw-year=|2020|2024. -
?search=tooine— free-form substring search oversearchablemirror columns (case-insensitive). -
?sort=name/?sort=-name— ascending / descending; resolved to a GSI viasortableIndices -
?offset=N&limit=N— offset pagination -
?cursor[=token]— cursor pagination onGET /: constant cost per page, nototal, no links; keep paging while the response carriescursor. Malformed tokens →400 BadCursor -
?format=jsonl— NDJSON list output onGET /(jsonis the default; anything else →400 BadFormat) -
?keys=A,B,C— for the/-by-keysroutes (namesis the legacy alias) -
?max-items=N/?resume=<token>— resumable mass ops on the mass routes -
?confirm=yes|true|1|on— explicit opt-in for an unscopedDELETE / -
?consistent=yes|true|1|on— strong consistency -
?force=yes|true|1|on— overwrite without existence check onput/clone/move
Wire casing: multi-word query params are kebab-case (max-items); envelope keys are camelCase.
-
POST /— JSON object: the new item. -
PUT /:key— JSON object: replacement fields (URL key merged in automatically). -
PATCH /:key— JSON object: partial update; meta keys (_delete,_separator,_arrayOps) are extracted as patch options. -
PUT /-load— JSON array: items to bulk-load. -
PUT /-clone//-move//:key/-clone//:key/-move— JSON object: overlay merged into each cloned/moved item. -
PUT /-clone-by-keys//-move-by-keys— JSON object overlay; keys come from?keys=…query. -
DELETE /-by-keys— keys from?keys=…; if absent, accepts a JSON array body.
interface HandlerOptions {
policy?: RestPolicyOverrides; // partial policy; envelope / statusCodes merge key-by-key
sortableIndices?: Record<string, string>;
keyFromPath?: (rawKey: string, adapter: Adapter) => Record<string, unknown>;
exampleFromContext?: (query: Record<string, string>, body: unknown) => Record<string, unknown>;
maxBodyBytes?: number;
}-
policy— overrides the REST core defaults (envelope keys, status codes, prefixes, paging defaults). See REST core for the full shape. -
sortableIndices—{<sort field name>: <GSI index name>}. Resolves?sort=nameto{index: '-t-name-index', descending: false}. Without an entry,?sort=...is ignored. ('-t-name-index'here is author convention for the indexed GSI name; the toolkit only uses the name as a literal.) -
keyFromPath(rawKey, adapter)— convert the URL:keysegment into a key object.rawKeyis the URL-decoded segment string;adaptergives access toadapter.keyFieldsso composite-key implementations know which fields to populate. Return a full key object — every entry inadapter.keyFieldsmust be a property. Default:(raw, adapter) => ({[adapter.keyFields[0]]: raw}). Override for composite keys (e.g. split"tenant:user"on:), or to coerce types (numeric partition keys, UUID validation). -
exampleFromContext(query, body)— build theexampleargument passed to the Adapter'sprepareListInput(example, index)hook.queryis the parsed URL query object (Record<string, string>),bodyis the parsed request body (unknown— typically a JSON object). Return a partial item that the hook will use to seed aKeyConditionExpression(e.g.{tenant: query.tenant}for a per-tenant list). Default returns{}— useful whenprepareListInputderives everything fromindexalone. -
maxBodyBytes— maximum request-body size in bytes. Bodies exceeding this are rejected with HTTP 413PayloadTooLarge. Defaults to 1 MB (1048576). Defends against memory-exhaustion DoS from unbounded streaming bodies. Raise for consumers that legitimately send larger payloads (bulk/-loadroutes are a common reason); lower for tighter REST endpoints.
Unhandled errors flow through the policy's mapErrorStatus and errorBody. Default response shape: {code, message}. See REST core for the SDK error → HTTP status table.
If a handler throws an Error with a numeric .status between 400 and 599, that status is honored verbatim instead of being mapped (useful for the body parser's 400 BadJsonBody, the 405 MethodNotAllowed for unsupported routes, etc.).
When the result has total, the envelope includes links.prev / links.next (relative URLs against the current request). At edges, the corresponding link is null.
To customize URL construction (e.g., absolute URLs with a base path), set a custom policy.envelope.links key or build the response manually using paginationLinks + buildEnvelope from REST core.
import {createServer} from 'node:http';
import {DynamoDBClient} from '@aws-sdk/client-dynamodb';
import {DynamoDBDocumentClient} from '@aws-sdk/lib-dynamodb';
import {Adapter} from 'dynamodb-toolkit';
import {createHandler} from 'dynamodb-toolkit/handler';
const client = new DynamoDBClient({region: 'us-east-1'});
const docClient = DynamoDBDocumentClient.from(client, {marshallOptions: {removeUndefinedValues: true}});
const adapter = new Adapter({
client: docClient,
table: 'planets',
keyFields: ['name'],
searchable: {name: 1, climate: 1, terrain: 1},
hooks: {
prepare(item, isPatch) {
const out = {...item};
for (const k of Object.keys(this.searchable)) {
if (k in item) out['-search-' + k] = String(item[k]).toLowerCase();
}
if (!isPatch) out['-t'] = 1;
return out;
},
revive(item) {
const out = {};
for (const k of Object.keys(item)) if (!k.startsWith('-')) out[k] = item[k];
return out;
},
prepareListInput(_, index) {
const idx = index || '-t-name-index';
return {
IndexName: idx,
KeyConditionExpression: '#t = :t',
ExpressionAttributeNames: {'#t': '-t'},
ExpressionAttributeValues: {':t': 1}
};
}
}
});
const handler = createHandler(adapter, {sortableIndices: {name: '-t-name-index'}});
createServer(handler).listen(3000);Ready-made bindings for Express, Koa, Fetch runtimes (Cloudflare Workers, Deno Deploy, Bun.serve, Hono), and AWS Lambda ship as subpath exports with the same wire contract — see Framework adapters. You can also skip the route pack entirely and call Adapter methods from hand-written routes — see Plain Express routes. For anything else, wire your framework's request to the same rest-core parsers and builders; the handler implementation in src/handler/handler.js serves as a reference.
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