http-response-kit 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +357 -272
  3. package/dist/chunk-373JVXMN.mjs +1199 -0
  4. package/dist/chunk-373JVXMN.mjs.map +1 -0
  5. package/dist/chunk-FFRB2EUE.mjs +36 -0
  6. package/dist/chunk-FFRB2EUE.mjs.map +1 -0
  7. package/dist/express.d.mts +50 -0
  8. package/dist/express.d.ts +50 -0
  9. package/dist/express.js +1155 -0
  10. package/dist/express.js.map +1 -0
  11. package/dist/express.mjs +28 -0
  12. package/dist/express.mjs.map +1 -0
  13. package/dist/fastify.d.mts +45 -0
  14. package/dist/fastify.d.ts +45 -0
  15. package/dist/fastify.js +1170 -0
  16. package/dist/fastify.js.map +1 -0
  17. package/dist/fastify.mjs +43 -0
  18. package/dist/fastify.mjs.map +1 -0
  19. package/dist/hono.d.mts +33 -0
  20. package/dist/hono.d.ts +33 -0
  21. package/dist/hono.js +1139 -0
  22. package/dist/hono.js.map +1 -0
  23. package/dist/hono.mjs +18 -0
  24. package/dist/hono.mjs.map +1 -0
  25. package/dist/index.d.mts +75 -493
  26. package/dist/index.d.ts +75 -493
  27. package/dist/index.js +480 -180
  28. package/dist/index.js.map +1 -0
  29. package/dist/index.mjs +18 -891
  30. package/dist/index.mjs.map +1 -0
  31. package/dist/kit-nHfihlKE.d.mts +753 -0
  32. package/dist/kit-nHfihlKE.d.ts +753 -0
  33. package/dist/koa.d.mts +38 -0
  34. package/dist/koa.d.ts +38 -0
  35. package/dist/koa.js +1150 -0
  36. package/dist/koa.js.map +1 -0
  37. package/dist/koa.mjs +24 -0
  38. package/dist/koa.mjs.map +1 -0
  39. package/dist/schemas.d.mts +452 -0
  40. package/dist/schemas.d.ts +452 -0
  41. package/dist/schemas.js +124 -0
  42. package/dist/schemas.js.map +1 -0
  43. package/dist/schemas.mjs +119 -0
  44. package/dist/schemas.mjs.map +1 -0
  45. package/dist/shared-Czwmz6Xa.d.ts +22 -0
  46. package/dist/shared-DYq1Fic4.d.mts +22 -0
  47. package/package.json +104 -49
package/README.md CHANGED
@@ -1,272 +1,357 @@
1
- <p align="center">
2
- <img src="https://raw.githubusercontent.com/matteo-teodori/http-response-kit/main/logo.png" alt="http-response-kit logo" width="300" />
3
- </p>
4
-
5
- > 🚀 A professional HTTP error and response formatting library for Node.js
6
-
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
- [![Node.js](https://img.shields.io/badge/Node.js-16+-green.svg)](https://nodejs.org/)
9
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
-
11
- ## Features
12
-
13
- - ✅ **Complete HTTP Status Codes** - All 4xx and 5xx error codes + 1xx, 2xx, 3xx
14
- - ✅ **TypeScript First** - Full type safety and IntelliSense support
15
- - ✅ **Factory Methods** - Convenient `HttpError.notFound()`, `HttpError.badRequest()`, etc.
16
- - ✅ **Customizable** - Override default messages, add metadata, configure globally
17
- - ✅ **Zero Dependencies** - Lightweight and fast
18
- - ✅ **ESM & CommonJS** - Works everywhere
19
-
20
- ## Installation
21
-
22
- ```bash
23
- npm install http-response-kit
24
- ```
25
-
26
- ## Quick Start
27
-
28
- ```typescript
29
- import { HttpError, HttpResponse, configure } from 'http-response-kit';
30
-
31
- // Throw HTTP errors
32
- throw HttpError.notFound('User not found');
33
- throw HttpError.badRequest('Invalid email format');
34
- throw HttpError.unauthorized('Token expired');
35
-
36
- // Or use status codes directly
37
- throw new HttpError(404, { message: 'User not found' });
38
- throw new HttpError(429, { message: 'Slow down!', retryAfter: 60 });
39
-
40
- // Format success responses
41
- const response = HttpResponse.success({
42
- data: { id: 1, name: 'John' },
43
- message: 'User retrieved successfully'
44
- });
45
- // { success: true, status_code: 200, data: {...}, message: '...' }
46
-
47
- // Format error responses
48
- const error = HttpError.notFound('User not found');
49
- const errorResponse = HttpResponse.error(error);
50
- // { success: false, status_code: 404, error: { type: 'not_found', ... } }
51
- ```
52
-
53
- ## Usage with Express
54
-
55
- ```typescript
56
- import express from 'express';
57
- import { HttpError, HttpResponse, configure } from 'http-response-kit';
58
-
59
- const app = express();
60
-
61
- // Configure library
62
- configure({
63
- isDevelopment: process.env.NODE_ENV === 'development',
64
- includeTimestamp: true,
65
- });
66
-
67
- // Your routes
68
- app.get('/users/:id', async (req, res) => {
69
- try {
70
- const user = await findUser(req.params.id);
71
- if (!user) {
72
- throw HttpError.notFound('User not found');
73
- }
74
- res.json(HttpResponse.ok(user));
75
- } catch (err) {
76
- const error = HttpError.fromError(err);
77
- res.status(error.code).json(HttpResponse.error(error));
78
- }
79
- });
80
-
81
- // Global error handler
82
- app.use((err, req, res, next) => {
83
- const error = HttpError.fromError(err);
84
- res.status(error.code).json(HttpResponse.error(error));
85
- });
86
- ```
87
-
88
- ## API Reference
89
-
90
- ### HttpSuccessCode (1xx Informational)
91
-
92
- | Enum | Code | Description |
93
- |------|------|-------------|
94
- | `CONTINUE` | 100 | Continue with request |
95
- | `SWITCHING_PROTOCOLS` | 101 | Switching protocols |
96
- | `PROCESSING` | 102 | Processing request |
97
- | `EARLY_HINTS` | 103 | Early hints |
98
-
99
- ### HttpSuccessCode (2xx Success)
100
-
101
- | Enum | Code | Description |
102
- |------|------|-------------|
103
- | `OK` | 200 | Request succeeded |
104
- | `CREATED` | 201 | Resource created |
105
- | `ACCEPTED` | 202 | Request accepted for processing |
106
- | `NON_AUTHORITATIVE_INFORMATION` | 203 | Non-authoritative information |
107
- | `NO_CONTENT` | 204 | No content to return |
108
- | `RESET_CONTENT` | 205 | Reset content |
109
- | `PARTIAL_CONTENT` | 206 | Partial content |
110
- | `MULTI_STATUS` | 207 | Multi-status response |
111
- | `ALREADY_REPORTED` | 208 | Already reported |
112
- | `IM_USED` | 226 | IM used |
113
-
114
- ### HttpRedirectCode (3xx Redirect)
115
-
116
- | Enum | Code | Description |
117
- |------|------|-------------|
118
- | `MULTIPLE_CHOICES` | 300 | Multiple choices available |
119
- | `MOVED_PERMANENTLY` | 301 | Resource moved permanently |
120
- | `FOUND` | 302 | Resource found at different URI |
121
- | `SEE_OTHER` | 303 | See other resource |
122
- | `NOT_MODIFIED` | 304 | Resource not modified |
123
- | `USE_PROXY` | 305 | Use proxy |
124
- | `TEMPORARY_REDIRECT` | 307 | Temporary redirect |
125
- | `PERMANENT_REDIRECT` | 308 | Permanent redirect |
126
-
127
- ### HttpError
128
-
129
- #### Factory Methods (4xx Client Errors)
130
-
131
- | Method | Code | Description |
132
- |--------|------|-------------|
133
- | `HttpError.badRequest()` | 400 | Bad Request |
134
- | `HttpError.unauthorized()` | 401 | Unauthorized |
135
- | `HttpError.paymentRequired()` | 402 | Payment Required |
136
- | `HttpError.forbidden()` | 403 | Forbidden |
137
- | `HttpError.notFound()` | 404 | Not Found |
138
- | `HttpError.methodNotAllowed()` | 405 | Method Not Allowed |
139
- | `HttpError.notAcceptable()` | 406 | Not Acceptable |
140
- | `HttpError.proxyAuthenticationRequired()` | 407 | Proxy Auth Required |
141
- | `HttpError.requestTimeout()` | 408 | Request Timeout |
142
- | `HttpError.conflict()` | 409 | Conflict |
143
- | `HttpError.gone()` | 410 | Gone |
144
- | `HttpError.lengthRequired()` | 411 | Length Required |
145
- | `HttpError.preconditionFailed()` | 412 | Precondition Failed |
146
- | `HttpError.payloadTooLarge()` | 413 | Payload Too Large |
147
- | `HttpError.uriTooLong()` | 414 | URI Too Long |
148
- | `HttpError.unsupportedMediaType()` | 415 | Unsupported Media Type |
149
- | `HttpError.rangeNotSatisfiable()` | 416 | Range Not Satisfiable |
150
- | `HttpError.expectationFailed()` | 417 | Expectation Failed |
151
- | `HttpError.imATeapot()` | 418 | I'm a Teapot |
152
- | `HttpError.misdirectedRequest()` | 421 | Misdirected Request |
153
- | `HttpError.unprocessableEntity()` | 422 | Unprocessable Entity |
154
- | `HttpError.locked()` | 423 | Locked |
155
- | `HttpError.failedDependency()` | 424 | Failed Dependency |
156
- | `HttpError.tooEarly()` | 425 | Too Early |
157
- | `HttpError.upgradeRequired()` | 426 | Upgrade Required |
158
- | `HttpError.preconditionRequired()` | 428 | Precondition Required |
159
- | `HttpError.tooManyRequests()` | 429 | Too Many Requests |
160
- | `HttpError.requestHeaderFieldsTooLarge()` | 431 | Header Fields Too Large |
161
- | `HttpError.unavailableForLegalReasons()` | 451 | Unavailable For Legal Reasons |
162
-
163
- #### Factory Methods (5xx Server Errors)
164
-
165
- | Method | Code | Description |
166
- |--------|------|-------------|
167
- | `HttpError.internalServerError()` | 500 | Internal Server Error |
168
- | `HttpError.notImplemented()` | 501 | Not Implemented |
169
- | `HttpError.badGateway()` | 502 | Bad Gateway |
170
- | `HttpError.serviceUnavailable()` | 503 | Service Unavailable |
171
- | `HttpError.gatewayTimeout()` | 504 | Gateway Timeout |
172
- | `HttpError.httpVersionNotSupported()` | 505 | HTTP Version Not Supported |
173
- | `HttpError.variantAlsoNegotiates()` | 506 | Variant Also Negotiates |
174
- | `HttpError.insufficientStorage()` | 507 | Insufficient Storage |
175
- | `HttpError.loopDetected()` | 508 | Loop Detected |
176
- | `HttpError.bandwidthLimitExceeded()` | 509 | Bandwidth Limit Exceeded |
177
- | `HttpError.notExtended()` | 510 | Not Extended |
178
- | `HttpError.networkAuthenticationRequired()` | 511 | Network Auth Required |
179
-
180
- #### Utility Methods
181
-
182
- ```typescript
183
- // Convert any error to HttpError
184
- const httpError = HttpError.fromError(unknownError);
185
-
186
- // Check if error is HttpError
187
- if (HttpError.isHttpError(error)) { ... }
188
-
189
- // Check error category
190
- error.isClientError(); // 4xx
191
- error.isServerError(); // 5xx
192
-
193
- // Add metadata
194
- throw HttpError.badRequest('Validation failed', {
195
- fields: ['email', 'password']
196
- });
197
- ```
198
-
199
- ### HttpResponse
200
-
201
- ```typescript
202
- // Success responses
203
- HttpResponse.success({ data, message, statusCode, metadata });
204
- HttpResponse.ok(data, message);
205
- HttpResponse.created(data, message);
206
- HttpResponse.accepted(data, message);
207
- HttpResponse.noContent();
208
-
209
- // Error responses
210
- HttpResponse.error(httpError);
211
- HttpResponse.fromError(anyError);
212
-
213
- // Paginated responses
214
- HttpResponse.paginated(items, {
215
- page: 1,
216
- limit: 10,
217
- total: 100
218
- });
219
- ```
220
-
221
- ### Configuration
222
-
223
- ```typescript
224
- import { configure } from 'http-response-kit';
225
-
226
- configure({
227
- // Enable stack traces in error responses
228
- isDevelopment: true,
229
-
230
- // Include timestamps in all responses
231
- includeTimestamp: true,
232
-
233
- // Custom default messages per status code
234
- customMessages: {
235
- 404: 'Oops! This page went on vacation 🏝️',
236
- 500: 'Well, this is embarrassing... 🤖'
237
- },
238
-
239
- // Custom response transformer
240
- responseTransformer: (response) => ({
241
- ...response,
242
- api_version: 'v1'
243
- })
244
- });
245
- ```
246
-
247
- ## Status Code Enums
248
-
249
- ```typescript
250
- import {
251
- HttpSuccessCode,
252
- HttpClientErrorCode,
253
- HttpServerErrorCode,
254
- HttpRedirectCode,
255
- HttpInfoCode
256
- } from 'http-response-kit';
257
-
258
- // Use enums for type safety
259
- HttpSuccessCode.OK // 200
260
- HttpSuccessCode.CREATED // 201
261
- HttpSuccessCode.NO_CONTENT // 204
262
- HttpClientErrorCode.NOT_FOUND // 404
263
- HttpServerErrorCode.INTERNAL_SERVER_ERROR // 500
264
- ```
265
-
266
- ## Changelog
267
-
268
- See [CHANGELOG.md](CHANGELOG.md) for detailed release notes.
269
-
270
- ## License
271
-
272
- MIT © [Matteo Teodori](https://github.com/matteo-teodori)
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/matteo-teodori/http-response-kit/main/logo.png" alt="http-response-kit logo" width="300" />
3
+ </p>
4
+
5
+ > 🚀 Framework-agnostic HTTP error & response standardization for Node.js — secure by default, RFC 9457 ready.
6
+
7
+ [![CI](https://github.com/matteo-teodori/http-response-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/matteo-teodori/http-response-kit/actions/workflows/ci.yml)
8
+ [![npm version](https://img.shields.io/npm/v/http-response-kit.svg)](https://www.npmjs.com/package/http-response-kit)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
10
+ [![Node.js](https://img.shields.io/badge/Node.js-20+-green.svg)](https://nodejs.org/)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+
13
+ ## Features
14
+
15
+ - ✅ **Secure by default** 5xx error messages are never leaked to clients unless explicitly exposed (`expose` flag, à la industry best practice)
16
+ - ✅ **RFC 9457 Problem Details** — first-class `application/problem+json` support (`toProblemDetails()`, `format: 'problem'`)
17
+ - ✅ **Domain error catalogs** — stable machine-readable codes (`USER_NOT_FOUND`) decoupled from HTTP status
18
+ - ✅ **Structured validation errors** standard `errors: [{ field, message, code }]` shape
19
+ - ✅ **Zero global state** — every `createResponseKit()` is an isolated instance; safe for multi-tenant / multi-package setups
20
+ - ✅ **Framework adapters** — Express, Fastify, Koa, Hono via subpath exports (still zero dependencies)
21
+ - ✅ **Socket/system error mapping** — ECONNREFUSED, ETIMEDOUT, DNS and undici/fetch failures automatically become the correct 502/503/504
22
+ - ✅ **Correlation IDs** — `request_id` support end-to-end, header extraction in adapters
23
+ - **HTTP header helpers** — `Retry-After`, `WWW-Authenticate`, custom headers via `getHeaders()`
24
+ - ✅ **Offset & cursor pagination**, i18n message resolver, metadata sanitizer hook
25
+ - ✅ **JSON Schema / OpenAPI 3.1 components** — `http-response-kit/schemas` for contract testing
26
+ - **Complete HTTP status codes**, TypeScript-first, ESM & CommonJS, zero dependencies
27
+ - ✅ **Agent-ready** — ships an installable Agent Skill (`npx skills add matteo-teodori/http-response-kit`) so AI coding agents follow the project conventions
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install http-response-kit
33
+ ```
34
+
35
+ Requires Node.js >= 20 (Node 18 reached end-of-life in April 2025).
36
+
37
+ ## Quick Start
38
+
39
+ One entry point: `createResponseKit()`. Every kit is an isolated formatter that
40
+ owns its configuration — the library has **no global mutable state**.
41
+
42
+ ```typescript
43
+ import { createResponseKit, HttpError } from 'http-response-kit';
44
+
45
+ const api = createResponseKit(); // sane defaults, configure only what you need
46
+
47
+ // Throw HTTP errors anywhere in your code
48
+ throw HttpError.notFound('User not found');
49
+ throw HttpError.badRequest('Invalid email format');
50
+ throw new HttpError(429, { message: 'Slow down!', retryAfter: 60 });
51
+
52
+ // Structured validation errors
53
+ throw HttpError.validation([
54
+ { field: 'email', message: 'Invalid email format', code: 'invalid_format' },
55
+ ]);
56
+
57
+ // Format success responses (typed with generics)
58
+ interface User { id: number; name: string }
59
+ const response = api.ok<User>({ id: 1, name: 'John' }, 'User retrieved');
60
+ // { success: true, status_code: 200, data: User, message: '...' }
61
+
62
+ // Format error responses
63
+ const errorResponse = api.error(HttpError.notFound('User not found'));
64
+ // { success: false, status_code: 404, error: { type: 'not_found', message: '...', ... } }
65
+ ```
66
+
67
+ ## Security model (important)
68
+
69
+ `HttpError` carries an `expose` flag: `true` for 4xx, **`false` for 5xx by default**.
70
+ When an error is not exposable, serialization replaces the message with the generic
71
+ status description and omits `metadata` — so internal details (DB errors, connection
72
+ strings, stack info) never reach clients. The original message stays on the instance
73
+ for logging.
74
+
75
+ ```typescript
76
+ const err = HttpError.fromError(new Error('ECONNREFUSED db:5432'));
77
+ err.message; // 'ECONNREFUSED db:5432' → for your logs
78
+ api.error(err).error.message; // 'An unexpected error occurred on the server.'
79
+
80
+ // Explicit opt-in when a 5xx message IS meant for clients:
81
+ throw new HttpError(503, { message: 'Maintenance until 17:00 UTC', expose: true });
82
+ ```
83
+
84
+ Stack traces and cause chains are only serialized in development mode.
85
+
86
+ ## RFC 9457 Problem Details
87
+
88
+ Emit standards-compliant `application/problem+json` bodies:
89
+
90
+ ```typescript
91
+ import { PROBLEM_CONTENT_TYPE } from 'http-response-kit';
92
+
93
+ const problem = api.problem(HttpError.notFound('User not found'), {
94
+ typeBase: 'https://errors.example.com',
95
+ instance: '/users/42',
96
+ requestId: 'req-123',
97
+ });
98
+ // {
99
+ // type: 'https://errors.example.com/not_found',
100
+ // title: 'Not Found',
101
+ // status: 404,
102
+ // detail: 'User not found',
103
+ // instance: '/users/42',
104
+ // request_id: 'req-123'
105
+ // }
106
+
107
+ res.status(problem.status).set('Content-Type', PROBLEM_CONTENT_TYPE).json(problem);
108
+ ```
109
+
110
+ Or switch the whole output to Problem Details (adapters honor it automatically):
111
+
112
+ ```typescript
113
+ const api = createResponseKit({ format: 'problem', problemTypeBase: 'https://errors.example.com' });
114
+ ```
115
+
116
+ ## Socket & system errors
117
+
118
+ `HttpError.fromError()` (and therefore every adapter) recognizes Node.js
119
+ system errors including the undici codes thrown by Node 18+ `fetch`, even
120
+ when buried in the `cause` chain and maps them to the semantically correct
121
+ status instead of a generic 500:
122
+
123
+ | Cause | Status |
124
+ |-------|--------|
125
+ | `ECONNREFUSED`, `ECONNRESET`, `EPIPE`, `ENOTFOUND`, `EHOSTUNREACH`, TLS failures | 502 Bad Gateway |
126
+ | `ETIMEDOUT`, `UND_ERR_CONNECT_TIMEOUT`, `UND_ERR_HEADERS_TIMEOUT` | 504 Gateway Timeout |
127
+ | `EAI_AGAIN`, `EMFILE`, `ENOBUFS` | 503 Service Unavailable |
128
+
129
+ ```typescript
130
+ try {
131
+ await fetch('http://upstream.internal/api');
132
+ } catch (err) {
133
+ throw HttpError.fromError(err);
134
+ // -> 502, error.code: 'ECONNREFUSED', client sees only the generic message,
135
+ // full "connect ECONNREFUSED 10.0.0.5:80" stays on the instance for logs
136
+ }
137
+ ```
138
+
139
+ The mapping (`SystemErrorStatusMap`) and the standalone `mapSystemError()` are
140
+ exported if you need custom logic.
141
+
142
+ ## Framework adapters
143
+
144
+ Zero-dependency adapters via subpath exports:
145
+
146
+ ```typescript
147
+ // Express
148
+ import { errorHandler, notFoundHandler } from 'http-response-kit/express';
149
+ app.use(notFoundHandler());
150
+ app.use(errorHandler({ onError: (err, requestId) => logger.error({ err, requestId }) }));
151
+
152
+ // Fastify (maps AJV validation errors to structured issues automatically)
153
+ import { fastifyErrorHandler, fastifyNotFoundHandler } from 'http-response-kit/fastify';
154
+ app.setErrorHandler(fastifyErrorHandler());
155
+ app.setNotFoundHandler(fastifyNotFoundHandler());
156
+
157
+ // Koa (register FIRST)
158
+ import { koaErrorHandler } from 'http-response-kit/koa';
159
+ app.use(koaErrorHandler());
160
+
161
+ // Hono
162
+ import { honoErrorHandler } from 'http-response-kit/hono';
163
+ app.onError(honoErrorHandler());
164
+ ```
165
+
166
+ Every adapter: sets error headers (`Retry-After`, ...), extracts `x-request-id`
167
+ (configurable) and echoes it as `request_id`, supports an `onError` logging hook,
168
+ and emits Problem Details when `format: 'problem'` is configured.
169
+
170
+ ## Configuration
171
+
172
+ Each kit owns its configuration two packages or tenants in the same process can
173
+ never step on each other:
174
+
175
+ ```typescript
176
+ import { createResponseKit } from 'http-response-kit';
177
+
178
+ const kit = createResponseKit({
179
+ isDevelopment: process.env.NODE_ENV === 'development',
180
+ casing: 'camel', // statusCode / retryAfter instead of snake_case
181
+ format: 'problem',
182
+ problemTypeBase: 'https://errors.example.com',
183
+ messageResolver: (err) => i18n.t(`errors.${err.type}`), // i18n hook
184
+ metadataSanitizer: (m) => omit(m, ['password', 'token']), // PII/secrets filter
185
+ });
186
+
187
+ kit.ok(user);
188
+ kit.error(HttpError.notFound(), { requestId: req.id });
189
+ ```
190
+
191
+ ## Domain error catalog
192
+
193
+ Stable application-level error codes, independent from HTTP status:
194
+
195
+ ```typescript
196
+ import { createErrorCatalog } from 'http-response-kit';
197
+
198
+ export const Errors = createErrorCatalog({
199
+ USER_NOT_FOUND: { status: 404, message: 'User does not exist' },
200
+ PLAN_LIMIT_REACHED: { status: 402, message: 'Upgrade your plan' },
201
+ GATEWAY_DOWN: { status: 502 }, // 5xx → not exposed by default
202
+ });
203
+
204
+ throw Errors.USER_NOT_FOUND();
205
+ // response: { error: { type: 'not_found', code: 'USER_NOT_FOUND', ... } }
206
+ // problem: { code: 'USER_NOT_FOUND', ... }
207
+ ```
208
+
209
+ ## Validation errors
210
+
211
+ ```typescript
212
+ throw HttpError.validation([
213
+ { field: 'email', message: 'Invalid email format', code: 'invalid_format' },
214
+ { field: 'age', message: 'Must be >= 18', code: 'too_small' },
215
+ ]);
216
+ // → 422 with error.errors: [{ field, message, code }] (also in Problem Details)
217
+ ```
218
+
219
+ ## HTTP headers
220
+
221
+ ```typescript
222
+ const err = new HttpError(401, {
223
+ headers: { 'WWW-Authenticate': 'Bearer realm="api"' },
224
+ });
225
+ err.getHeaders(); // { 'WWW-Authenticate': 'Bearer realm="api"' }
226
+
227
+ HttpError.tooManyRequests('Slow down', 30).getHeaders(); // { 'Retry-After': '30' }
228
+ ```
229
+
230
+ Adapters set these headers automatically.
231
+
232
+ ## Pagination
233
+
234
+ ```typescript
235
+ // Offset-based
236
+ api.paginated(users, { page: 2, limit: 20, total: 105 });
237
+ // metadata.pagination: { page, limit, total, total_pages, has_next, has_prev }
238
+
239
+ // Cursor-based
240
+ api.paginatedCursor(users, { nextCursor: 'eyJpZCI6NDJ9', limit: 20 });
241
+ // metadata.pagination: { next_cursor, limit, has_next, has_prev }
242
+ ```
243
+
244
+ ## Request correlation
245
+
246
+ ```typescript
247
+ api.success({ data: user, requestId: req.headers['x-request-id'] });
248
+ api.error(err, { requestId: req.headers['x-request-id'] });
249
+ // → responses include request_id; adapters do this automatically
250
+ ```
251
+
252
+ ## JSON Schema & OpenAPI
253
+
254
+ ```typescript
255
+ import {
256
+ successResponseSchema,
257
+ errorResponseSchema,
258
+ problemDetailsSchema,
259
+ openApiComponents,
260
+ } from 'http-response-kit/schemas';
261
+
262
+ // contract testing, gateway validation, or:
263
+ const spec = { openapi: '3.1.0', /* ... */ components: openApiComponents };
264
+ ```
265
+
266
+ ## Configuration reference
267
+
268
+ ```typescript
269
+ createResponseKit({
270
+ isDevelopment: false, // stack traces + cause chains in responses
271
+ includeTimestamp: true, // ISO timestamp on every response
272
+ format: 'standard', // 'standard' | 'problem' (RFC 9457)
273
+ casing: 'snake', // 'snake' | 'camel' output keys
274
+ problemTypeBase: undefined, // base URI for problem `type`
275
+ exposeServerErrors: false, // never enable in production
276
+ customMessages: { 404: 'Not here' }, // default messages per status code
277
+ messageResolver: (err) => undefined, // i18n hook
278
+ metadataSanitizer: (m) => m, // strip PII/secrets
279
+ responseTransformer: (r) => r, // last-mile shape transformer
280
+ });
281
+ ```
282
+
283
+ `kit.configure(partial)` merges further settings into that instance only.
284
+
285
+ ## Usage with Express (manual, without adapter)
286
+
287
+ ```typescript
288
+ const api = createResponseKit();
289
+
290
+ app.get('/users/:id', async (req, res, next) => {
291
+ try {
292
+ const user = await findUser(req.params.id);
293
+ if (!user) throw HttpError.notFound('User not found');
294
+ res.json(api.ok(user));
295
+ } catch (err) {
296
+ next(err);
297
+ }
298
+ });
299
+
300
+ app.use((err, req, res, next) => {
301
+ const error = HttpError.fromError(err);
302
+ res.status(error.code).json(api.error(error));
303
+ });
304
+ ```
305
+
306
+ ## Migrating from 1.x
307
+
308
+ 2.0.0 is a clean break with a definitive API. What changed:
309
+
310
+ 1. **`HttpResponse` and `configure()` are gone.** Create a kit once and use it everywhere: `const api = createResponseKit(config)`. All `HttpResponse.x()` statics exist as `api.x()` with identical signatures; `isSuccess`/`isError` are now the standalone `isSuccessResponse`/`isErrorResponse`.
311
+ 2. **5xx messages are sanitized by default.** If a server-error message must reach clients, set `expose: true` on that error.
312
+ 3. **`metadata` is omitted for non-exposable errors.**
313
+ 4. **`customMessages` moved to the kit** and act as per-status default messages at serialization time.
314
+ 5. `HttpError.fromStatus()` removed — use `new HttpError(code, options)`.
315
+ 6. `engines.node` is now `>= 20` (Node 16/18 are end-of-life); `cause` is the native ES2022 `Error.cause`.
316
+
317
+ ## API Reference
318
+
319
+ Full status-code tables (1xx–5xx), enums (`HttpErrorCode`, `HttpSuccessCode`, ...),
320
+ and definitions are exported and documented via TSDoc — your editor's IntelliSense
321
+ shows the complete reference. Key entry points:
322
+
323
+ | Export | Purpose |
324
+ |--------|---------|
325
+ | `createResponseKit()` | THE entry point: isolated formatter instances (`ok`, `error`, `problem`, `paginated`, ...) |
326
+ | `HttpError` | Error class + 40 factory methods + `validation()`, `fromError()`, `getHeaders()`, `toProblemDetails()` |
327
+ | `createErrorCatalog()` | Typed domain error factories with stable codes |
328
+ | `isSuccessResponse()` / `isErrorResponse()` | Response type guards |
329
+ | `PROBLEM_CONTENT_TYPE`, `toProblem()`, `isProblemDetails()` | RFC 9457 helpers |
330
+ | `http-response-kit/{express,fastify,koa,hono}` | Framework adapters |
331
+ | `http-response-kit/schemas` | JSON Schema / OpenAPI 3.1 components |
332
+
333
+ ## Using with AI agents
334
+
335
+ The repo ships an [Agent Skill](skills/http-response-kit/SKILL.md) that teaches
336
+ coding agents (Claude Code, Cursor, and any SKILL.md-compatible tool) the
337
+ project conventions: kit-only API, `expose` security semantics, error catalogs,
338
+ structured validation, RFC 9457 mode and the framework adapters.
339
+
340
+ Install it in your project:
341
+
342
+ ```bash
343
+ npx skills add matteo-teodori/http-response-kit
344
+ ```
345
+
346
+ Benchmarked effect: on refactoring/integration tasks, agents *without* the
347
+ skill tend to hallucinate the library API (invented error classes, removed
348
+ v1 statics, hand-rolled problem+json); with the skill they follow the v2
349
+ conventions correctly. In our eval suite the assertion pass rate went from
350
+ 15% to 100%.
351
+
352
+ Machine-readable response contracts are also available for validation and
353
+ OpenAPI tooling via `http-response-kit/schemas`.
354
+
355
+ ## Contributing & Security
356
+
357
+ See [CONTRIBUTING.md](CONTRIBUTING