Skip to content
This repository was archived by the owner on Jul 17, 2026. It is now read-only.

Options

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

Options

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

interface FetchAdapterOptions<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, request: Request) => Record<string, unknown>;
  maxBodyBytes?: number;
  mountPath?: string;
  onMiss?: (request: Request) => Response | null | Promise<Response | null> | undefined | void;
}

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.

createFetchAdapter(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).

createFetchAdapter(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]]: raw})

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

// Composite "partition:sort" keys
createFetchAdapter(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};
  }
});

// Numeric partition keys
createFetchAdapter(adapter, {
  keyFromPath: (raw, a) => {
    const n = Number(raw);
    if (!Number.isFinite(n)) throw Object.assign(new Error('Numeric key expected'), {status: 400});
    return {[a.keyFields[0]]: n};
  }
});

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, request) => exampleObject

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

createFetchAdapter(adapter, {
  exampleFromContext: (query, _body, request) => ({
    tenant: request.headers.get('x-tenant-id') || 'default',
    status: query.status || 'active'
  })
});

The three args:

  • query — parsed URL query as Record<string, string>. URLSearchParams duplicates collapse to the first value.
  • body — parsed request body, unknown. Only defined for PUT /-clone / PUT /-move, which have a body; null on GET / / DELETE /.
  • request — the full Fetch Request. Inspect request.headers, request.url, request.method, and host-specific context (e.g. Cloudflare Workers' request.cf). request.body is already consumed by the time this runs — don't try to re-read it.

See Per-tenant filtering for a complete cookbook.

maxBodyBytes

Byte cap for request bodies. Enforced on every body-reading route (POST, PUT, PATCH, DELETE with body) via two mechanisms:

  1. Content-Length fast-path reject. If the declared Content-Length exceeds the cap, the handler rejects 413 PayloadTooLarge before reading any bytes.
  2. Streaming byte counter. Otherwise the body is read through request.body.getReader() with a running byte total; the handler rejects 413 as soon as the counter crosses the cap — catches chunked uploads and liar-Content-Length requests.

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

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

// Dedicated bulk-load endpoint — allow larger bodies
createFetchAdapter(bulkAdapter, {maxBodyBytes: 16 * 1024 * 1024});

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

See Body reading for the full flow.

mountPath

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

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

// Terminal Workers handler — adapter owns the entire URL.
const handler = createFetchAdapter(planets);

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

Match rules:

  • Exact match (pathname === mountPath) → route is /.
  • Prefix match (pathname.startsWith(mountPath + '/')) → route is pathname.slice(mountPath.length).
  • Anything else → miss path (see onMiss).

Matching is case-sensitive. No trailing-slash normalization beyond what URL parsing does.

onMiss

Hook invoked when the request can't be dispatched to a known route:

  • Pathname is outside mountPath, or
  • matchRoute returns 'unknown' for this method + path.

Without onMiss, unknown / off-mount requests resolve to an empty 404 Response — the handler is terminal. Suitable for single-collection Workers and Deno.serve(handler).

With onMiss supplied, the handler's return type widens from Promise<Response> to Promise<Response | null>, and the hook decides:

  • Returns Response → handler returns it.
  • Returns null → handler returns null, so the caller can try the next handler in a composed router.
  • Returns undefined / void → fall back to the default 404 Response.
  • Returns Promise<…> → the above semantics applied to the resolved value.
// Router composition — yield control back to Hono / itty-router on a miss.
const planets = createFetchAdapter(adapter, {mountPath: '/planets', onMiss: () => null});

// Custom miss response — e.g. send something friendlier than an empty 404.
const planets = createFetchAdapter(adapter, {
  mountPath: '/planets',
  onMiss: request => new Response(JSON.stringify({
    error: 'not found',
    path: new URL(request.url).pathname
  }), {status: 404, headers: {'content-type': 'application/json'}})
});

See Composition for full integration patterns.

Interaction with parent HandlerOptions

The options surface is a direct 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 a third request argument. Callbacks written against the core handler still work (extra args ignored).
  • mountPath and onMiss — Fetch-specific. No equivalents in the core handler because node:http servers don't compose on the handler surface.

Clone this wiki locally