@pagopa/hexagonal-fastify
v0.2.0
Published
Fastify primary-adapter primitives for the IO platform hexagonal architecture: request validation, response/error wiring and code-first route mounting.
Readme
@pagopa/hexagonal-fastify
Fastify primary-adapter primitives for the IO platform hexagonal architecture.
Wire framework-agnostic use cases, validators and formatters (from
@pagopa/hexagonal-core) onto Fastify, and mount code-first
route contracts (defineRoute, from @pagopa/hexagonal-core/adapters) — without
coupling your domain to the framework. Error mapping is delegated to the core
error mapper and never re-implemented here. The OpenAPI registry stays in
@pagopa/hexagonal-openapi; this adapter is
registry-free.
Installation
pnpm add @pagopa/hexagonal-fastify @pagopa/hexagonal-core fastify zod neverthrow @standard-schema/specfastify, zod, neverthrow and @standard-schema/spec are peer dependencies.
What's inside
createHttpHandler— turns aUseCase+ input validator + success responder into a Fastify handler, replying withapplication/problem+jsonon errors.mountFastifyRoute— mounts adefineRoutecontract on a Fastify instance, deriving validation, the single success status and response encoding from the contract. Both 2xx codes and301/302redirects count as success; a contract declaring more than one success response is rejected at mount time. An optionaloutputMappermaps the use-case output to the success schema's input before encoding; redirect / no-body (204) responses strip the body.createFastifyRequestValidator/fastifyExtractPayload— bind the core Standard Schema validator toFastifyRequest(pathfromrequest.params).sendErrorResponse— write a domain error as RFC 7807 problem+json (delegates to the core error mapper).
Usage
import { defineRoute, ProblemJson } from "@pagopa/hexagonal-core/adapters";
import { NotFoundError } from "@pagopa/hexagonal-core/domain/errors";
import { mountFastifyRoute } from "@pagopa/hexagonal-fastify";
import Fastify from "fastify";
import { err, ok } from "neverthrow";
import { z } from "zod";
const UserSchema = z.object({ id: z.string(), name: z.string() }).meta({
id: "User",
});
const app = Fastify();
mountFastifyRoute(app, {
contract: defineRoute({
method: "get",
operationId: "getUser",
path: "/users/{id}", // OpenAPI syntax; converted to Fastify ":id"
request: { path: z.object({ id: z.string() }) },
response: { 200: UserSchema, 404: ProblemJson },
}),
inputMapper: (req) => ({ id: req.path.id }),
useCase: async ({ id }) =>
id === "1"
? ok({ id: "1", name: "Alice" })
: err(new NotFoundError("User", id)),
});
await app.listen({ port: 3000 });A request to /users/1 returns 200 with the validated user; /users/999
returns 404 with an application/problem+json body.
Scripts
| Command | Description |
| -------------------- | ------------------------------ |
| pnpm build | Dual ESM + CJS build (tsdown) |
| pnpm typecheck | Type-check without emitting |
| pnpm lint | ESLint autofix |
| pnpm test | Run tests with Vitest |
| pnpm test:coverage | Run tests with coverage report |
| pnpm clean | Remove dist/ |
