NestJS adapter
@smonn/ids/nestjs provides ParseIdPipe — a PipeTransform that validates an
untrusted route param against a codec and returns the canonical Id<Brand> to the
handler. @nestjs/common is an optional peer dependency.
pnpm add @nestjs/commonimport { ParseIdPipe } from "@smonn/ids/nestjs";import { type Id, createTimestampId } from "@smonn/ids";import { Controller, Get, Param, Query } from "@nestjs/common";
const usr = createTimestampId("usr");const thing = createTimestampId("thg");
@Controller("users")class UsersController { @Get(":id") findOne(@Param("id", new ParseIdPipe(usr)) id: Id<"usr">) { return { id }; // Id<"usr">, canonical }}
// ParseIdPipe is source-agnostic — works with @Query just as well as @Param@Controller("users")class UserSearchController { @Get() list(@Query("userId", new ParseIdPipe(usr)) userId: Id<"usr">) { return { userId }; // Id<"usr">, canonical }}
// Status remap without a full handler@Controller("things")class ThingsController { @Get(":id") findOne(@Param("id", new ParseIdPipe(thing, { status: { brand_mismatch: 400 } })) id: Id<"thg">) { return { id }; }}- Default error channel: on failure the pipe throws
NotFoundException(404) for brand mismatches orBadRequestException(400) for malformed IDs. options.onError: custom escape hatch — must throw or re-throw becausetransform()has no HTTP context to write a response inline.options.status: remaps the default HTTP status for a failure reason.
IdParamFailure shape
Section titled “IdParamFailure shape”The onError callback receives an IdParamFailure — a discriminated union on reason:
type IdParamFailure = | { readonly reason: "brand_mismatch"; readonly status: number } | { readonly reason: "malformed"; readonly status: number };reason: "brand_mismatch"— the ID has a valid structure but belongs to a different brand; defaultstatusis 404.reason: "malformed"— the ID is syntactically invalid; defaultstatusis 400.statusreflects any override set viaoptions.status, otherwise the default above.
IdParamFailure is re-exported from @smonn/ids/nestjs — no separate import is needed.
onError escape hatch
Section titled “onError escape hatch”Unlike Hono or Express, PipeTransform.transform receives only the raw value and
ArgumentMetadata — there is no HTTP context. The onError hook is therefore typed as
(failure: IdParamFailure) => never; it must throw or re-throw rather than writing a
response inline. If the hook returns without throwing, the pipe still throws the default
NestJS exception for that failure (absent a custom status override, NotFoundException for
brand mismatches, BadRequestException for malformed IDs).
import { UnprocessableEntityException } from "@nestjs/common";
const pipe = new ParseIdPipe(usr, { onError: (failure) => { throw new UnprocessableEntityException(`ID invalid: ${failure.reason}`); },});Signature verification
Section titled “Signature verification”Pass verify: true together with a Signed Timestamp codec or a Wrapped key codec to authenticate the tag. When verify: true is set, transform() returns Promise<Id<Brand>> instead of Id<Brand> — NestJS awaits this automatically. TypeScript enforces the codec requirement via constructor overloads — { verify: true } is a type error when paired with a non-verifiable codec (Timestamp, Opaque, Reverse Timestamp, Digest).
import { ParseIdPipe } from "@smonn/ids/nestjs";import { type Id } from "@smonn/ids";import { createSignedTimestampId, importSigningKey } from "@smonn/ids/signed";import { Controller, Get, Param } from "@nestjs/common";
const key = await importSigningKey(new Uint8Array(32));const usr = createSignedTimestampId("usr", { keys: [key] });
@Controller("users")class UsersController { @Get(":id") findOne(@Param("id", new ParseIdPipe(usr, { verify: true })) id: Id<"usr">) { return { id }; // Id<"usr">, structurally parsed AND HMAC-verified }}When verify: true is set:
- The pipe first runs
codec.safeParse— a parse failure follows the normal exception path. - If parsing succeeds,
codec.safeVerify(value)is awaited. A tag failure is treated asreason: "malformed"(throwsBadRequestExceptionby default, overrideable viaoptions.status.malformed).
For the Signed Timestamp codec, safeVerify checks the HMAC tag. For the Wrapped key codec, safeVerify is a verify-only alias of safeUnwrap (it drops the recovered lookup key); a wrong-key, tampered, or revoked-key ID surfaces as the same "malformed" failure.
Without verify: true, transform() is synchronous — the default behaviour is unchanged.
400 vs 404 defaults
Section titled “400 vs 404 defaults”- Brand mismatch (
invalid_prefix) →reason: "brand_mismatch", status 404. Ausr_ID makes no sense on/orders/:id— the resource cannot exist under this route. - Malformed or missing ID (
invalid_base32/not_string) →reason: "malformed", status 400.
ParseIdPipe calls safeParse at the boundary (lenient: mixed case and Crockford aliases),
so the handler always receives a canonical, normalized Id<Brand>. Works with any codec
variant’s structural safeParse.