Skip to content

idParam

idParam<ParamKey, Brand>(paramName, codec, options?): (request, reply) => Promise<void>

Defined in: src/adapters/fastify.ts:147

Fastify preHandler hook factory that validates a named route param against a codec via safeParse.

Default (no options): throws IdParamError carrying statusCode and reason so the app’s existing setErrorHandler controls rendering. The adapter does not write a response body itself.

options.onError: when provided, the adapter awaits the hook on validation failure. If the hook sends a response (reply.sent is true after it resolves), the adapter takes no further action. Otherwise, the adapter falls back to throwing IdParamError, so the route handler never runs with an invalid ID.

options.status: remaps the default HTTP status for a reason without a full handler.

  • Brand mismatch (invalid_prefix) → reason: "brand_mismatch", default 404
  • Malformed or missing ID → reason: "malformed", default 400

Storage: on success, stores the canonical Id<Brand> in request.params[paramName] by mutating the params object in place. This contrasts with the Express adapter, which writes to res.locals[paramName]. Fastify writes to request.params in place because Fastify’s preHandler lifecycle runs before the route handler and its request object is the idiomatic side-channel for enriched request data; there is no reply.locals equivalent in Fastify.

Return type note: the returned hook is typed as (request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>. Assigning it to a Fastify preHandler slot is backward-compatible (method-signature bivariance applies). However, a locally-annotated variable typed as the bare (request: FastifyRequest, reply: FastifyReply) => Promise<void> will produce a TypeScript error under --strictFunctionTypes because function parameter types are contravariant. Use preHandler assignment or let TypeScript infer the type to avoid this.

ParamKey extends string

Brand extends string

ParamKey

IdVerifiableCodec<Brand>

IdParamVerifyOptions

(request, reply) => Promise<void>

import { idParam, IdParamError } from "@smonn/ids/fastify";
import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");
// Default: throws IdParamError → setErrorHandler renders it
fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
const id = request.params.id; // string (compile-time); Id<"usr"> at runtime after preHandler
});
// Error handler receives the typed error
fastify.setErrorHandler((err, request, reply) => {
if (err instanceof IdParamError) {
reply.status(err.statusCode).send({ error: err.reason });
return;
}
reply.send(err);
});
// Override: consumer fully owns the error response
fastify.get("/orgs/:id", {
preHandler: idParam("id", org, {
onError: (failure, request, reply) =>
reply.status(failure.status).send({ error: failure.reason }),
}),
}, handler);
// Or a lightweight status remap without a full handler
fastify.get("/things/:id", {
preHandler: idParam("id", thing, { status: { brand_mismatch: 400 } }),
}, handler);

idParam<ParamKey, Brand>(paramName, codec, options?): (request, reply) => Promise<void>

Defined in: src/adapters/fastify.ts:155

Fastify preHandler hook factory that validates a named route param against a codec via safeParse.

Default (no options): throws IdParamError carrying statusCode and reason so the app’s existing setErrorHandler controls rendering. The adapter does not write a response body itself.

options.onError: when provided, the adapter awaits the hook on validation failure. If the hook sends a response (reply.sent is true after it resolves), the adapter takes no further action. Otherwise, the adapter falls back to throwing IdParamError, so the route handler never runs with an invalid ID.

options.status: remaps the default HTTP status for a reason without a full handler.

  • Brand mismatch (invalid_prefix) → reason: "brand_mismatch", default 404
  • Malformed or missing ID → reason: "malformed", default 400

Storage: on success, stores the canonical Id<Brand> in request.params[paramName] by mutating the params object in place. This contrasts with the Express adapter, which writes to res.locals[paramName]. Fastify writes to request.params in place because Fastify’s preHandler lifecycle runs before the route handler and its request object is the idiomatic side-channel for enriched request data; there is no reply.locals equivalent in Fastify.

Return type note: the returned hook is typed as (request: FastifyRequest<{ Params: Record<string, Id<Brand>> }>, reply: FastifyReply) => Promise<void>. Assigning it to a Fastify preHandler slot is backward-compatible (method-signature bivariance applies). However, a locally-annotated variable typed as the bare (request: FastifyRequest, reply: FastifyReply) => Promise<void> will produce a TypeScript error under --strictFunctionTypes because function parameter types are contravariant. Use preHandler assignment or let TypeScript infer the type to avoid this.

ParamKey extends string

Brand extends string

ParamKey

IdCodec<Brand>

IdParamOptions

(request, reply) => Promise<void>

import { idParam, IdParamError } from "@smonn/ids/fastify";
import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");
// Default: throws IdParamError → setErrorHandler renders it
fastify.get("/users/:id", { preHandler: idParam("id", usr) }, (request, reply) => {
const id = request.params.id; // string (compile-time); Id<"usr"> at runtime after preHandler
});
// Error handler receives the typed error
fastify.setErrorHandler((err, request, reply) => {
if (err instanceof IdParamError) {
reply.status(err.statusCode).send({ error: err.reason });
return;
}
reply.send(err);
});
// Override: consumer fully owns the error response
fastify.get("/orgs/:id", {
preHandler: idParam("id", org, {
onError: (failure, request, reply) =>
reply.status(failure.status).send({ error: failure.reason }),
}),
}, handler);
// Or a lightweight status remap without a full handler
fastify.get("/things/:id", {
preHandler: idParam("id", thing, { status: { brand_mismatch: 400 } }),
}, handler);