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.
- package/LICENSE +21 -21
- package/README.md +357 -272
- package/dist/chunk-373JVXMN.mjs +1199 -0
- package/dist/chunk-373JVXMN.mjs.map +1 -0
- package/dist/chunk-FFRB2EUE.mjs +36 -0
- package/dist/chunk-FFRB2EUE.mjs.map +1 -0
- package/dist/express.d.mts +50 -0
- package/dist/express.d.ts +50 -0
- package/dist/express.js +1155 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +28 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +45 -0
- package/dist/fastify.d.ts +45 -0
- package/dist/fastify.js +1170 -0
- package/dist/fastify.js.map +1 -0
- package/dist/fastify.mjs +43 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +33 -0
- package/dist/hono.d.ts +33 -0
- package/dist/hono.js +1139 -0
- package/dist/hono.js.map +1 -0
- package/dist/hono.mjs +18 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +75 -493
- package/dist/index.d.ts +75 -493
- package/dist/index.js +480 -180
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +18 -891
- package/dist/index.mjs.map +1 -0
- package/dist/kit-nHfihlKE.d.mts +753 -0
- package/dist/kit-nHfihlKE.d.ts +753 -0
- package/dist/koa.d.mts +38 -0
- package/dist/koa.d.ts +38 -0
- package/dist/koa.js +1150 -0
- package/dist/koa.js.map +1 -0
- package/dist/koa.mjs +24 -0
- package/dist/koa.mjs.map +1 -0
- package/dist/schemas.d.mts +452 -0
- package/dist/schemas.d.ts +452 -0
- package/dist/schemas.js +124 -0
- package/dist/schemas.js.map +1 -0
- package/dist/schemas.mjs +119 -0
- package/dist/schemas.mjs.map +1 -0
- package/dist/shared-Czwmz6Xa.d.ts +22 -0
- package/dist/shared-DYq1Fic4.d.mts +22 -0
- 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
|
-
> 🚀
|
|
6
|
-
|
|
7
|
-
[](https://github.com/matteo-teodori/http-response-kit/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/http-response-kit)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://nodejs.org/)
|
|
11
|
+
[](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
|