{"id":34502650,"url":"https://github.com/quiltjs/quilt","last_synced_at":"2025-12-24T02:11:29.382Z","repository":{"id":325953340,"uuid":"1102807389","full_name":"quiltjs/quilt","owner":"quiltjs","description":"Lightweight, type-safe request handling and routing for Node HTTP servers","archived":false,"fork":false,"pushed_at":"2025-11-24T21:21:41.000Z","size":2204,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-28T01:25:23.068Z","etag":null,"topics":["dependency-injection","esm","express","fastify","http-server","middleware","nodejs","request-handling","typescript","web-framework"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quiltjs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-24T04:20:12.000Z","updated_at":"2025-11-26T14:48:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/quiltjs/quilt","commit_stats":null,"previous_names":["quiltjs/quilt"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/quiltjs/quilt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiltjs%2Fquilt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiltjs%2Fquilt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiltjs%2Fquilt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiltjs%2Fquilt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quiltjs","download_url":"https://codeload.github.com/quiltjs/quilt/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quiltjs%2Fquilt/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27992997,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-24T02:00:07.193Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dependency-injection","esm","express","fastify","http-server","middleware","nodejs","request-handling","typescript","web-framework"],"created_at":"2025-12-24T02:11:28.693Z","updated_at":"2025-12-24T02:11:29.372Z","avatar_url":"https://github.com/quiltjs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @quiltjs/quilt\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/quilt.png\" alt=\"Quilt logo\" width=\"160\" /\u003e\n\u003c/p\u003e\n\nLightweight, type-safe request handling and routing for Node HTTP servers, with first-class Fastify and Express support.\n\nQuilt’s core idea is simple: **model each request as a dependency graph of small handlers**. Auth, validation, and loading run once per request and feed into your route handler via strong types, instead of being scattered across ad-hoc middleware.\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@quiltjs/quilt\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@quiltjs/quilt.svg\" alt=\"npm version\" /\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/node/v/@quiltjs/quilt.svg\" alt=\"node compatibility\" /\u003e\n  \u003cimg src=\"https://img.shields.io/npm/l/@quiltjs/quilt.svg\" alt=\"license: ISC\" /\u003e\n\u003c/p\u003e\n\n`@quiltjs/quilt` lets you build HTTP APIs from small, composable, strongly-typed “handlers”\ninstead of ad-hoc middleware that mutates `req`/`res`. It is designed to be framework-agnostic and\nto sit cleanly on top of your HTTP server of choice.\n\n- Strong TypeScript types for handlers and their dependencies\n- Explicit dependency graph instead of “magic” middleware ordering\n- Framework abstraction via `ServerEngineAdapter` (Fastify and Express adapters included)\n- Simple routing via `Quilt`\n- JSON and form-data support via your framework's middleware\n\nAt a glance, Quilt replaces chains of middleware like:\n\n```ts\napp.get('/profile', authMiddleware, loadUserMiddleware, (req, res) =\u003e {\n  if (!req.user) {\n    return res.status(401).json({ error: 'Unauthorized' });\n  }\n\n  res.json({ id: req.user.id, name: req.user.name });\n});\n```\n\nwith a small, typed dependency graph:\n\n```ts\nconst authHandler = createHandler({\n  execute: async ({ req }) =\u003e {\n    const userId = req.headers['x-user-id'];\n    if (!userId || Array.isArray(userId)) {\n      throw new Error('Unauthorized');\n    }\n    return { userId };\n  },\n});\n\nconst loadUserHandler = createHandler({\n  dependencies: { auth: authHandler },\n  execute: async (_ctx, deps) =\u003e {\n    const user = await loadUserById(deps.auth.userId);\n    if (!user) {\n      throw new Error('NotFound');\n    }\n    return user;\n  },\n});\n\nconst profileRouteHandler = createHandler({\n  dependencies: { user: loadUserHandler },\n  execute: async ({ res }, deps) =\u003e {\n    res.status(200).json({\n      id: deps.user.id,\n      name: deps.user.name,\n    });\n  },\n});\n\nquilt.get('/profile', profileRouteHandler);\n```\n\nEach handler is reusable, testable on its own, and runs at most once per request (even if multiple downstream handlers depend on it).\n\n## TL;DR\n\nYou keep Fastify/Express/Node HTTP and write small, typed handlers instead of ad-hoc middleware:\n\n```ts\nimport fastify from 'fastify';\nimport {\n  Quilt,\n  FastifyEngineAdapter,\n  createHandler,\n  type FastifyHttpContext,\n} from '@quiltjs/quilt';\n\nconst app = fastify();\nconst quilt = new Quilt(new FastifyEngineAdapter({ fastify: app }));\n\nconst hello = createHandler({\n  execute: async ({ req, res }: FastifyHttpContext) =\u003e {\n    res.code(200).send({ message: `Hello, ${req.query.name ?? 'world'}!` });\n  },\n});\n\nquilt.get('/api/hello', hello);\n```\n\n---\n\n## Status\n\nQuilt is currently in **early but production-ready** shape:\n\n- Core concepts (handlers, dependency graphs, adapters) are stable.\n- The public API surface is intentionally small and is expected to evolve carefully.\n- Breaking changes, when needed before `1.0`, will be documented in `CHANGELOG.md`.\n- Fastify and Express adapters are the primary, stable integrations; the Node HTTP adapter is a minimal but production-friendly option for simple HTTP servers.\n\nFeedback and real-world usage reports are very welcome via GitHub issues.\n\n---\n\n## API reference (overview)\n\nThis is a quick overview of the main exports. See the rest of this README for patterns and examples.\n\n- `createHandler` – defines a handler with optional dependencies and an `execute` function.\n- `executeHandler` – runs a handler graph for an arbitrary context (not just HTTP).\n- `Quilt` – routes HTTP methods (`get`, `post`, `put`, `patch`, `delete`, `options`, `head`) to handlers and lets you set a central error handler via `setErrorHandler`.\n\nTypes:\n\n- `Handler\u003cO, Ctx, D\u003e` – typed description of a handler’s output, context, and dependencies.\n- `HandlerOutputs\u003cD\u003e` – maps a handler’s `dependencies` to the inferred `deps` type.\n- `HttpContext\u003cReq, Res\u003e` – convenience type for `{ req, res }` contexts.\n- `FastifyHandler` / `ExpressHandler` – handler aliases for the Fastify and Express HTTP contexts.\n- `NodeHttpHandlerContext` / `NodeHttpHandler` / `createNodeHttpRouteHandler` – helpers for Node HTTP contexts with typed `params`, `query`, and `body`.\n- `HTTPMethod` – union of supported HTTP methods.\n- `ServerEngineAdapter\u003cReq, Res\u003e` – interface adapters implement to plug Quilt into different HTTP engines.\n\nAdapters:\n\n- `FastifyEngineAdapter` / `FastifyHttpContext` – Fastify integration (`req` / `reply`).\n- `ExpressEngineAdapter` / `ExpressHttpContext` – Express integration (`Request` / `Response`).\n- `NodeHttpEngineAdapter` / `NodeHttpContext` / `NodeHttpRequest` – minimal adapter for Node’s built-in `http` module.\n\nAll of these are exported from the main entrypoint:\n\n```ts\nimport {\n  Quilt,\n  createHandler,\n  executeHandler,\n  FastifyEngineAdapter,\n  type FastifyHttpContext,\n  ExpressEngineAdapter,\n  type ExpressHttpContext,\n  NodeHttpEngineAdapter,\n  type NodeHttpContext,\n  type NodeHttpRequest,\n} from '@quiltjs/quilt';\n```\n\nYou rarely need every export in a single file, but this shows the surface area at a glance.\n\n---\n\n## Who is Quilt for?\n\nQuilt is aimed at TypeScript teams who:\n\n- Already use Fastify, Express, or Node's `http` module.\n- Have grown past a handful of routes and are feeling middleware sprawl.\n- Want predictable composition and strong types without adopting a whole new framework.\n\nIt works best for medium-sized HTTP APIs where:\n\n- You want to factor shared concerns (auth, validation, loading) into small, reusable units.\n- You want to reuse those units across routes without copy-paste.\n- You still want to keep the underlying framework and its ecosystem.\n\n### When Quilt is (and isn’t) a good fit\n\nQuilt is a good fit when:\n\n- You have more than “a few” routes and shared concerns like auth, validation, and loading logic.\n- You want to keep Fastify/Express/Node HTTP, but make request logic more explicit and testable.\n- You care about strong TypeScript types across your request pipeline.\n\nQuilt is probably not the right tool when:\n\n- You have a tiny app with only a handful of routes and simple logic.\n- You’re already all-in on a batteries-included framework like NestJS, Next.js API routes, or Remix and are happy with their patterns.\n- You don’t need shared handler reuse or dependency graphs beyond what simple middleware already gives you.\n\n---\n\n## Why Quilt?\n\nQuilt gives you a clearer alternative to traditional middleware. Instead of relying on ordering and mutation, you build request logic from small, typed handlers with explicit dependencies. No decorators, no global DI, no FP overhead — just predictable composition.\n\n- Explicit dependencies — handlers declare what they need; Quilt runs them once per request and injects the results.\n- Plain async functions — no decorators, classes, or schema systems required.\n- Framework-agnostic — works with Fastify, Express, or any HTTP server via a tiny adapter.\n- Consistent handler model across frameworks — the same dependency graph pattern works everywhere.\n- Composable by design — auth, loading, validation, and business logic stay small and reusable.\n\nIf you want strong types and predictable composition without adopting a whole new framework, Quilt is designed for exactly that.\n\n### Quilt vs other approaches\n\n- **Plain Express/Fastify middleware** – Great for small apps but scales poorly as logic is hidden in implicit ordering and `req`/`res` mutation. Quilt keeps your chosen framework but introduces explicit, typed dependencies instead of shared mutation.\n- **Full frameworks (e.g. NestJS)** – Provide batteries included (modules, DI, decorators). Quilt stays much smaller: you keep your framework and ecosystem, and only adopt a focused composition layer.\n- **RPC stacks (e.g. tRPC)** – Optimised for tightly-coupled client/server TypeScript. Quilt is HTTP-first and framework-agnostic; it plays nicely with any client, not just TS.\n- **FP-heavy ecosystems** – If you like simple async functions more than algebraic effects or type-level wizardry, Quilt is intentionally minimal.\n\n## Installation\n\nFastify:\n\n```bash\nnpm install @quiltjs/quilt fastify\n# or\npnpm add @quiltjs/quilt fastify\n```\n\nExpress:\n\n```bash\nnpm install @quiltjs/quilt express\n# or\npnpm add @quiltjs/quilt express\n```\n\nFastify/Express are peer dependencies because Quilt can be used with other HTTP engines via custom\nadapters. They are marked as optional peers so you only need to install the stack you actually use:\n\n- Using **Express** only: install `express@^4.18.2` or `express@^5.0.0` and ensure your `@types/express` major version matches your Express major version.\n- Using **Fastify** only: install `fastify@^4.25.2` (and `@fastify/multipart` if you need multipart).\n- Using a **custom adapter**: you do not need Express or Fastify at all.\n\n### TypeScript / ESM quickstart\n\nQuilt is published as ESM-only. For a typical TypeScript + Node 18+ project, a minimal `tsconfig.json` that works well with Quilt looks like:\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2021\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  }\n}\n```\n\nMake sure your `package.json` is also configured for ESM (for example, `\"type\": \"module\"`), so you can use standard `import`/`export` syntax with `@quiltjs/quilt`.\n\n## Quick start (Fastify)\n\n```ts\nimport fastify from 'fastify';\nimport { Quilt, FastifyEngineAdapter, createHandler } from '@quiltjs/quilt';\n\nconst server = fastify();\n\nconst quilt = new Quilt(new FastifyEngineAdapter({ fastify: server }));\n\n// Simple handler that writes a JSON response with typed context\nconst helloHandler = createHandler({\n  execute: async ({ req, res }: FastifyHttpContext) =\u003e {\n    res.code(200).send({ message: `Hello, ${req.query.name ?? 'world'}!` });\n  },\n});\n\nquilt.get('/api/hello', helloHandler);\n\nawait server.listen({ host: '0.0.0.0', port: 3000 });\n```\n\nNow `GET /api/hello?name=Quilt` returns:\n\n```json\n{ \"message\": \"Hello, Quilt!\" }\n```\n\nFor a runnable example you can clone and start immediately, see\n`examples/fastify-starter` in this repo.\n\n---\n\n## Quick start (Express)\n\n```ts\nimport express from 'express';\nimport {\n  Quilt,\n  ExpressEngineAdapter,\n  createHandler,\n  type ExpressHttpContext,\n} from '@quiltjs/quilt';\n\nconst app = express();\napp.use(express.json());\n\nconst quilt = new Quilt(new ExpressEngineAdapter({ app }));\n\nconst helloHandler = createHandler({\n  execute: async ({ req, res }: ExpressHttpContext) =\u003e {\n    res.status(200).json({\n      message: `Hello, ${req.query.name ?? 'world'}!`,\n    });\n  },\n});\n\nquilt.get('/api/hello', helloHandler);\n\napp.listen(3000, () =\u003e {\n  console.log('Server listening on http://localhost:3000');\n});\n```\n\nSee `examples/express-starter` for a complete runnable Express starter.\n\n---\n\n## Quick start (Node http)\n\n```ts\nimport http from 'node:http';\nimport {\n  Quilt,\n  NodeHttpEngineAdapter,\n  createNodeHttpRouteHandler,\n  type NodeHttpHandlerContext,\n} from '@quiltjs/quilt';\n\nconst adapter = new NodeHttpEngineAdapter();\nconst quilt = new Quilt(adapter);\n\ntype HelloParams = { name: string | undefined };\ntype HelloQuery = { search: string | undefined };\ntype HelloBody = { value: number };\n\nconst helloHandler = createNodeHttpRouteHandler\u003c\n  void,\n  HelloParams,\n  HelloQuery,\n  HelloBody\n\u003e({\n  execute: async ({ req, res }: NodeHttpHandlerContext\u003c\n    HelloParams,\n    HelloQuery,\n    HelloBody\n  \u003e) =\u003e {\n    res.statusCode = 200;\n    res.setHeader('content-type', 'application/json; charset=utf-8');\n    res.end(\n      JSON.stringify({\n        message: `Hello, ${req.query.name ?? 'world'}!`,\n      }),\n    );\n  },\n});\n\nquilt.get('/api/hello/:name', helloHandler);\n\nadapter.listen(3000, () =\u003e {\n  console.log('Server listening on http://localhost:3000');\n});\n```\n\nSee `examples/node-http-starter` for a complete runnable Node HTTP starter.\n\n---\n\n### Node HTTP metadata escape hatch\n\nFor most application data, Quilt encourages you to use handlers and their\ndependencies (`deps`) rather than mutating the request object. However,\nsome cross-cutting concerns (logging, tracing, correlation IDs) are easier\nto handle via per-request metadata.\n\nThe Node HTTP adapter exposes an optional `locals` bag on `NodeHttpRequest`:\n\n```ts\nexport type NodeHttpRequest = IncomingMessage \u0026 {\n  params: Record\u003cstring, string | undefined\u003e;\n  query: Record\u003cstring, string | undefined\u003e;\n  body: unknown;\n  locals?: Record\u003cstring, unknown\u003e;\n};\n```\n\nYou can treat this as an **escape hatch** for infrastructure metadata:\n\n```ts\nconst requestIdHandler = createNodeHttpRouteHandler({\n  execute: async ({ req }) =\u003e {\n    const requestId = crypto.randomUUID();\n    req.locals ??= {};\n    req.locals.requestId = requestId;\n    return { requestId };\n  },\n});\n\nconst routeHandler = createNodeHttpRouteHandler({\n  dependencies: { requestId: requestIdHandler },\n  execute: async ({ req, res }, deps) =\u003e {\n    console.log('handling', deps.requestId);\n    // ...\n  },\n});\n\nquilt.get('/api/with-request-id', routeHandler);\n```\n\nIn the error handler you can read the same metadata via `req.locals`:\n\n```ts\nquilt.setErrorHandler((error, { req, res }) =\u003e {\n  const requestId = req.locals?.requestId;\n  console.error('error for request', requestId, error);\n  // map error to HTTP response ...\n});\n```\n\nWe recommend using `locals` only for infrastructure concerns (logging,\ntracing, correlation IDs). Business data should continue to flow through\nhandlers and their `deps` so that dependencies remain explicit and\ntype-safe.\n\n---\n\n## Examples\n\nClone this repo and explore:\n\n- `examples/fastify-starter` – minimal Fastify + Quilt server with auth and error handling.\n- `examples/express-starter` – minimal Express + Quilt server using JSON responses and typed errors.\n- `examples/node-http-starter` – minimal Node HTTP + Quilt server with adapter-based routing and typed errors.\n- `examples/multi-route-api` – Fastify API with multiple routes sharing auth/validation/loading handlers across files.\n\n---\n\n## Core concepts\n\n### Handlers\n\nA **handler** is a small unit of work that:\n\n- Receives a context object (for HTTP adapters this is `{ req, res }`)\n- Optionally depends on other handlers\n- Produces an output that downstream handlers can consume\n\nYou usually create handlers via `createHandler`.\n\n```ts\nimport { createHandler } from '@quiltjs/quilt';\n\ntype RequestContext = {\n  headers: Record\u003cstring, string | string[] | undefined\u003e;\n};\n\n// Middleware-style handler that performs auth and returns user info\nconst authHandler = createHandler({\n  execute: async (ctx: RequestContext) =\u003e {\n    const userId = ctx.headers['x-user-id'];\n    if (!userId || Array.isArray(userId)) {\n      throw new Error('Unauthorized');\n    }\n    return { userId };\n  },\n});\n\n// Handler that depends on authHandler\nconst profileHandler = createHandler({\n  dependencies: { auth: authHandler },\n  execute: async (_ctx, deps) =\u003e {\n    return {\n      profileId: deps.auth.userId,\n      name: 'Jane Doe',\n    };\n  },\n});\n```\n\nHandlers form a directed acyclic graph. Quilt:\n\n- Topologically sorts the graph\n- Ensures each handler runs at most once per request\n- Caches outputs and injects them into downstream handlers as `deps`\n\n### Type safety and handler outputs\n\nHandlers are fully typed based on their dependencies. Quilt's `HandlerOutputs` type maps the `dependencies`\nobject into a strongly-typed `deps` parameter:\n\n```ts\nimport { createHandler, type Handler } from '@quiltjs/quilt';\n\ntype RequestContext = { requestId: string };\n\nconst requestIdHandler = createHandler({\n  execute: async (ctx: RequestContext) =\u003e ctx.requestId,\n});\n\nconst userHandler = createHandler({\n  dependencies: { requestId: requestIdHandler },\n  execute: async (_ctx: RequestContext, deps) =\u003e {\n    // deps.requestId is inferred as string\n    const id = deps.requestId;\n    return { id };\n  },\n});\n\nconst routeHandler: Handler\u003c\n  void,\n  RequestContext,\n  { user: typeof userHandler }\n\u003e = createHandler({\n  dependencies: { user: userHandler },\n  execute: async (_ctx, deps) =\u003e {\n    // deps.user.id is inferred as string\n    const ok: string = deps.user.id;\n\n    // @ts-expect-error user.id is not a number\n    const bad: number = deps.user.id;\n    void ok;\n    void bad;\n  },\n});\n```\n\nIf you change `requestIdHandler` to return a number, TypeScript will flag all downstream usages,\ngiving you a compile-time safety net over the entire handler graph.\n\n### Requests and responses\n\nIn practice you will usually model your **own** application-level input/output types and treat\nhandlers as an orchestration layer:\n\n- At the edge, handlers receive a context (for HTTP adapters this is `{ req, res }`).\n- They translate framework-specific request data into your own DTOs and call domain functions.\n- They write the HTTP response using the native framework APIs (`res.json`, `reply.send`, etc.).\n\n### Routing\n\nRouting is done via `Quilt`:\n\n- `Quilt` defines HTTP verb helpers (`get`, `post`, `put`, `patch`, `delete`, `options`, `head`)\n  and delegates to a `ServerEngineAdapter`.\n\n```ts\nimport { createHandler } from '@quiltjs/quilt';\n\nconst pingHandler = createHandler({\n  execute: async () =\u003e ({ ok: true }),\n});\n\nquilt.get('/status', pingHandler);\n```\n\n---\n\n## Opinionated patterns\n\nQuilt is most useful when you factor shared concerns into small, reusable handlers and compose them per route. These are the patterns we recommend in real apps.\n\n### 1. Domain errors for auth/validation/loading\n\nDefine a small set of domain errors once and reuse them across handlers:\n\n```ts\nclass UnauthorizedError extends Error {\n  constructor() {\n    super('Unauthorized');\n    this.name = 'UnauthorizedError';\n  }\n}\n\nclass BadRequestError extends Error {\n  constructor(message = 'Bad request') {\n    super(message);\n    this.name = 'BadRequestError';\n  }\n}\n\nclass NotFoundError extends Error {\n  constructor(message = 'Not found') {\n    super(message);\n    this.name = 'NotFoundError';\n  }\n}\n```\n\n### 2. Auth + validation + loading + response\n\nUse small handlers for each concern and compose them:\n\n```ts\nimport { createHandler } from '@quiltjs/quilt';\nimport { z } from 'zod';\n\n// 1. Auth: derive user id from the request\nconst authHandler = createHandler({\n  execute: async ({ req }) =\u003e {\n    const userId = req.headers['x-user-id'];\n    if (!userId || Array.isArray(userId)) {\n      throw new UnauthorizedError();\n    }\n    return { userId };\n  },\n});\n\n// 2. Validation: check input shape with Zod\nconst ParamsSchema = z.object({\n  id: z.string().min(1),\n});\n\nconst validateParamsHandler = createHandler({\n  execute: async ({ req }) =\u003e {\n    const result = ParamsSchema.safeParse(req.params);\n    if (!result.success) {\n      throw new BadRequestError('Invalid id parameter');\n    }\n    // result.data is strongly typed based on schema\n    return result.data;\n  },\n});\n\n// 3. Loading: fetch data based on validated input + auth\nconst loadOrderHandler = createHandler({\n  dependencies: { auth: authHandler, params: validateParamsHandler },\n  execute: async (_ctx, deps) =\u003e {\n    const order = await loadOrderForUser({\n      userId: deps.auth.userId,\n      orderId: deps.params.id,\n    });\n    if (!order) {\n      throw new NotFoundError('Order not found');\n    }\n    return order;\n  },\n});\n\n// 4. Business logic + HTTP response\nconst getOrderRoute = createHandler({\n  dependencies: { order: loadOrderHandler },\n  execute: async ({ res }, deps) =\u003e {\n    res.status(200).json({\n      id: deps.order.id,\n      total: deps.order.total,\n    });\n  },\n});\n\nquilt.get('/api/orders/:id', getOrderRoute);\n```\n\nEach handler does one thing and can be reused across routes (for example, `authHandler` and `validateParamsHandler`).\n\nQuilt does not ship its own schema or validation library on purpose. Instead, it is designed to work\nwith popular tools like Zod, Yup, Valibot, or your own validation layer.\n\n### 3. Central error handler (short-circuiting)\n\nMap domain errors to HTTP responses in one place:\n\n```ts\nimport { Quilt, FastifyEngineAdapter } from '@quiltjs/quilt';\n\nconst quilt = new Quilt(new FastifyEngineAdapter({ fastify: server }));\n\nquilt.setErrorHandler((error, { res }) =\u003e {\n  if (error instanceof UnauthorizedError) {\n    res.code(401).send({ error: 'Unauthorized' });\n    return;\n  }\n\n  if (error instanceof BadRequestError) {\n    res.code(400).send({ error: error.message });\n    return;\n  }\n\n  if (error instanceof NotFoundError) {\n    res.code(404).send({ error: error.message });\n    return;\n  }\n\n  console.error(error);\n  res.code(500).send({ error: 'Internal Server Error' });\n});\n```\n\nIf any handler throws, Quilt stops executing the graph and passes the error to your error handler. This is the recommended way to “short-circuit” a request based on auth, validation, or domain checks.\n\nThe same pattern works with Express via `res.status(...).json(...)`.\n\n---\n\n## Custom adapters\n\nFastify and Express support are provided out of the box via `FastifyEngineAdapter` and\n`ExpressEngineAdapter`, but you can integrate Quilt with any HTTP server by implementing\n`ServerEngineAdapter\u003cRequestType, ResponseType\u003e` yourself.\n\n---\n\n## Observability\n\n### Handler hooks\n\nBoth `executeHandler` and `Quilt` expose lightweight hooks you can use for logging, metrics, or tracing.\n\n- `executeHandler(handler, ctx, hooks)` accepts an optional `ExecuteHandlerHooks\u003cCtx\u003e`.\n- `Quilt` instances support `quilt.setHooks(hooks)`, where `hooks` has the same shape as `ExecuteHandlerHooks\u003cHttpContext\u003cReq, Res\u003e\u003e`.\n\nHooks are called once per handler execution with basic timing information:\n\n- `onHandlerStart({ handler, ctx })`\n- `onHandlerSuccess({ handler, ctx, durationMs, output })`\n- `onHandlerError({ handler, ctx, durationMs, error })`\n\nExample (Fastify):\n\n```ts\nimport fastify from 'fastify';\nimport { Quilt, FastifyEngineAdapter, createHandler } from '@quiltjs/quilt';\n\nconst app = fastify();\nconst quilt = new Quilt(new FastifyEngineAdapter({ fastify: app }));\n\nquilt.setHooks({\n  onHandlerSuccess: ({ durationMs }) =\u003e {\n    console.log(`[quilt] handler took ${durationMs.toFixed(3)}ms`);\n  },\n});\n\nconst helloHandler = createHandler({\n  execute: async ({ req, res }) =\u003e {\n    res.code(200).send({ message: `Hello, ${req.query.name ?? 'world'}!` });\n  },\n});\n\nquilt.get('/api/hello', helloHandler);\n```\n\nIf you use `executeHandler` directly (outside HTTP), you can pass `hooks` as the third argument:\n\n```ts\nawait executeHandler(handler, ctx, {\n  onHandlerSuccess: ({ durationMs }) =\u003e {\n    console.log('handler finished in', durationMs, 'ms');\n  },\n});\n```\n\n## TypeScript configuration\n\nQuilt is authored in TypeScript and ships declarations. A typical consumer `tsconfig.json` should\nwork fine as long as:\n\n- The module system is ESM (for example `\"module\": \"NodeNext\"`, `\"Node16\"`, or `\"ESNext\"`).\n- `\"moduleResolution\"` is compatible with Node-style ESM or modern bundlers (for example `\"NodeNext\"`, `\"Node16\"`, or `\"bundler\"`).\n- `\"strict\": true` is enabled to get the most out of the types.\n\nQuilt targets modern Node runtimes (Node 18+). It does not start your HTTP\nserver for you; instead, you should call your framework or adapter’s own\n`listen` method (for example, `app.listen` for Express or Fastify, or\n`adapter.listen` when using the Node HTTP adapter).\n\n### Using from CommonJS\n\n`@quiltjs/quilt` is published as ESM-only. In ESM modules you can import it as usual:\n\n```ts\nimport { Quilt, FastifyEngineAdapter, createHandler } from '@quiltjs/quilt';\n```\n\nIn a CommonJS module (for example, an existing Node app that still uses `require`), you can load Quilt via dynamic `import()`:\n\n```js\n// commonjs-app.js\nasync function main() {\n  const { Quilt, FastifyEngineAdapter, createHandler } = await import(\n    '@quiltjs/quilt'\n  );\n\n  // use Quilt as normal here\n}\n\nmain().catch((err) =\u003e {\n  console.error(err);\n  process.exit(1);\n});\n```\n\nIf you use TypeScript, we recommend configuring your project for ESM (`\"module\": \"NodeNext\"` or `\"ESNext\"`) so that your `.ts` files can use standard `import`/`export` syntax with Quilt.\n\n---\n\n## License\n\nLicensed under the ISC license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquiltjs%2Fquilt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquiltjs%2Fquilt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquiltjs%2Fquilt/lists"}