Skip to content

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.

Terminal window
pnpm add @nestjs/common
import { 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 or BadRequestException (400) for malformed IDs.
  • options.onError: custom escape hatch — must throw or re-throw because transform() has no HTTP context to write a response inline.
  • options.status: remaps the default HTTP status for a failure reason.

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; default status is 404.
  • reason: "malformed" — the ID is syntactically invalid; default status is 400.
  • status reflects any override set via options.status, otherwise the default above.

IdParamFailure is re-exported from @smonn/ids/nestjs — no separate import is needed.

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}`);
},
});

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:

  1. The pipe first runs codec.safeParse — a parse failure follows the normal exception path.
  2. If parsing succeeds, codec.safeVerify(value) is awaited. A tag failure is treated as reason: "malformed" (throws BadRequestException by default, overrideable via options.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.

  • Brand mismatch (invalid_prefix) → reason: "brand_mismatch", status 404. A usr_ 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.