@fastify-utils/error 0.0.0-stage → 1.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.
@@ -0,0 +1,26 @@
1
+ [WARN] Failed to replace env in config: ${NODE_AUTH_TOKEN} in .npmrc key "_authToken"
2
+ $ tsdown --config=tsdown.config.mts
3
+ ℹ tsdown v0.23.1 powered by rolldown v1.2.14
4
+ ℹ config file: /home/runner/work/fastify-utils/fastify-utils/packages/error/tsdown.config.mts
5
+ ℹ entry: ./src/index.ts
6
+ ℹ target: node24.0.0
7
+ ℹ tsconfig: tsconfig.json
8
+ ℹ Build start
9
+
10
+ WARN We recommend using the ESM format instead of CommonJS.
11
+ The ESM format is compatible with modern platforms and runtimes, and most new libraries are now distributed only in ESM format.
12
+ Learn more at https://nodejs.org/en/learn/modules/publishing-a-package#how-did-we-get-here
13
+
14
+
15
+ WARN TypeScript 7.0 does not yet have a stable API and is experimental. Some options will be unavailable.
16
+
17
+ ℹ Emit types with typescript@7.0.2
18
+ ℹ [CJS] out/index.cjs 24.17 kB │ gzip: 3.73 kB
19
+ ℹ [CJS] 1 files, total: 24.17 kB
20
+ ℹ [CJS] out/index.d.cts 15.01 kB │ gzip: 1.95 kB
21
+ ℹ [CJS] 1 files, total: 15.01 kB
22
+ ✔ Build complete in 663ms
23
+ ℹ [ESM] out/index.mjs 24.02 kB │ gzip: 3.67 kB
24
+ ℹ [ESM] out/index.d.mts 15.01 kB │ gzip: 1.95 kB
25
+ ℹ [ESM] 2 files, total: 39.03 kB
26
+ ✔ Build complete in 665ms
@@ -0,0 +1,109 @@
1
+ [WARN] Failed to replace env in config: ${NODE_AUTH_TOKEN} in .npmrc key "_authToken"
2
+ $ tsc --noEmit && vitest run
3
+
4
+  RUN  v5.0.5 /home/runner/work/fastify-utils/fastify-utils/packages/error
5
+ Coverage enabled with v8
6
+
7
+ ✓ src/core/http-error.test.ts (8 tests) 7ms
8
+ ✓ src/helpers/create-error-by-status/index.test.ts (18 tests) 15ms
9
+ ✓ src/errors/client/index.test.ts (28 tests) 15ms
10
+ ✓ src/errors/server/factories.test.ts (11 tests) 10ms
11
+ ✓ src/helpers/is-valid-rfc9457-json/index.test.ts (12 tests) 6ms
12
+ ✓ src/errors/client/factories.test.ts (9 tests) 10ms
13
+ ✓ src/errors/cause-propagation.test.ts (9 tests) 10ms
14
+ ✓ src/errors/server/index.test.ts (11 tests) 10ms
15
+ ✓ src/helpers/get-error-type/index.test.ts (5 tests) 7ms
16
+ ✓ src/helpers/normalize-to-string/index.test.ts (12 tests) 20ms
17
+ ✓ src/helpers/is-http-error/index.test.ts (6 tests) 5ms
18
+ ✓ src/helpers/extract-cause/index.test.ts (3 tests) 5ms
19
+ ✓ src/helpers/index.test.ts (6 tests) 6ms
20
+ ✓ src/config/configure.test.ts (3 tests) 5ms
21
+ ✓ src/index.test.ts (4 tests) 4ms
22
+ ✓ src/core/index.test.ts (2 tests) 5ms
23
+ ✓ src/config/index.test.ts (2 tests) 3ms
24
+
25
+  Test Files  17 passed (17)
26
+  Tests  149 passed (149)
27
+  Start at  06:15:36
28
+  Duration  1.45s (transform 38%, import 31%, worker 21%, tests 9%)
29
+
30
+  Isolate  17 workers spawned · ~122ms startup each (spawn + environment, per file)
31
+   at least ~567ms faster with isolate: false — reuses workers across files instead of one per file
32
+
33
+  % Coverage report from v8
34
+ -------------------|---------|----------|---------|---------|-------------------
35
+ File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
36
+ -------------------|---------|----------|---------|---------|-------------------
37
+ All files | 100 | 100 | 100 | 100 |
38
+ src | 100 | 100 | 100 | 100 |
39
+ error.ts | 100 | 100 | 100 | 100 |
40
+ errors.ts | 100 | 100 | 100 | 100 |
41
+ index.ts | 0 | 0 | 0 | 0 |
42
+ src/config | 100 | 100 | 100 | 100 |
43
+ configure.ts | 100 | 100 | 100 | 100 |
44
+ error-config.ts | 100 | 100 | 100 | 100 |
45
+ get-config.ts | 100 | 100 | 100 | 100 |
46
+ index.ts | 0 | 0 | 0 | 0 |
47
+ src/core | 100 | 100 | 100 | 100 |
48
+ http-error.ts | 100 | 100 | 100 | 100 |
49
+ index.ts | 0 | 0 | 0 | 0 |
50
+ src/errors/client | 100 | 100 | 100 | 100 |
51
+ ...tion-error.ts | 100 | 100 | 100 | 100 |
52
+ ...tion-error.ts | 100 | 100 | 100 | 100 |
53
+ ...uest-error.ts | 100 | 100 | 100 | 100 |
54
+ ...lict-error.ts | 100 | 100 | 100 | 100 |
55
+ ...iled-error.ts | 100 | 100 | 100 | 100 |
56
+ factories.ts | 100 | 100 | 100 | 100 |
57
+ ...ency-error.ts | 100 | 100 | 100 | 100 |
58
+ gone-error.ts | 100 | 100 | 100 | 100 |
59
+ index.ts | 0 | 0 | 0 | 0 |
60
+ ...ired-error.ts | 100 | 100 | 100 | 100 |
61
+ locked-error.ts | 100 | 100 | 100 | 100 |
62
+ ...owed-error.ts | 100 | 100 | 100 | 100 |
63
+ ...uest-error.ts | 100 | 100 | 100 | 100 |
64
+ ...able-error.ts | 100 | 100 | 100 | 100 |
65
+ ...ound-error.ts | 100 | 100 | 100 | 100 |
66
+ ...arge-error.ts | 100 | 100 | 100 | 100 |
67
+ ...ired-error.ts | 100 | 100 | 100 | 100 |
68
+ ...iled-error.ts | 100 | 100 | 100 | 100 |
69
+ ...ired-error.ts | 100 | 100 | 100 | 100 |
70
+ ...ired-error.ts | 100 | 100 | 100 | 100 |
71
+ ...able-error.ts | 100 | 100 | 100 | 100 |
72
+ ...imit-error.ts | 100 | 100 | 100 | 100 |
73
+ ...arge-error.ts | 100 | 100 | 100 | 100 |
74
+ ...eout-error.ts | 100 | 100 | 100 | 100 |
75
+ ...arly-error.ts | 100 | 100 | 100 | 100 |
76
+ ...sons-error.ts | 100 | 100 | 100 | 100 |
77
+ ...type-error.ts | 100 | 100 | 100 | 100 |
78
+ ...ired-error.ts | 100 | 100 | 100 | 100 |
79
+ ...long-error.ts | 100 | 100 | 100 | 100 |
80
+ ...tion-error.ts | 100 | 100 | 100 | 100 |
81
+ src/errors/server | 100 | 100 | 100 | 100 |
82
+ ...eway-error.ts | 100 | 100 | 100 | 100 |
83
+ factories.ts | 100 | 100 | 100 | 100 |
84
+ ...eout-error.ts | 100 | 100 | 100 | 100 |
85
+ ...rted-error.ts | 100 | 100 | 100 | 100 |
86
+ index.ts | 0 | 0 | 0 | 0 |
87
+ ...rage-error.ts | 100 | 100 | 100 | 100 |
88
+ ...rver-error.ts | 100 | 100 | 100 | 100 |
89
+ ...cted-error.ts | 100 | 100 | 100 | 100 |
90
+ ...ired-error.ts | 100 | 100 | 100 | 100 |
91
+ ...nded-error.ts | 100 | 100 | 100 | 100 |
92
+ ...nted-error.ts | 100 | 100 | 100 | 100 |
93
+ ...able-error.ts | 100 | 100 | 100 | 100 |
94
+ ...ates-error.ts | 100 | 100 | 100 | 100 |
95
+ src/helpers | 0 | 0 | 0 | 0 |
96
+ index.ts | 0 | 0 | 0 | 0 |
97
+ ...rror-by-status | 100 | 100 | 100 | 100 |
98
+ index.ts | 100 | 100 | 100 | 100 |
99
+ .../extract-cause | 100 | 100 | 100 | 100 |
100
+ index.ts | 100 | 100 | 100 | 100 |
101
+ ...get-error-type | 100 | 100 | 100 | 100 |
102
+ index.ts | 100 | 100 | 100 | 100 |
103
+ .../is-http-error | 100 | 100 | 100 | 100 |
104
+ index.ts | 100 | 100 | 100 | 100 |
105
+ ...d-rfc9457-json | 100 | 100 | 100 | 100 |
106
+ index.ts | 100 | 100 | 100 | 100 |
107
+ ...lize-to-string | 100 | 100 | 100 | 100 |
108
+ index.ts | 100 | 100 | 100 | 100 |
109
+ -------------------|---------|----------|---------|---------|-------------------
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xcfio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -1,3 +1,440 @@
1
- # Temporary Holding Version
1
+ # @fastify-utils/error
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TypeScript-first error handling package implementing [RFC 9457 Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html).
4
+
5
+ > RFC 9457 obsoletes [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807.html) - this package implements the latest specification.
6
+
7
+ ## Features
8
+
9
+ - **RFC 9457 Compliant** - Strictly follows the Problem Details specification
10
+ - **TypeScript First** - Full type safety with excellent IDE support
11
+ - **Auto-normalization** - Accepts `unknown` errors and normalizes them automatically
12
+ - **Categorized API** - Clean, readable error handling with `errors.client.*` and `errors.server.*`
13
+ - **39 Standard HTTP Errors** - Complete coverage of all standard HTTP error codes
14
+ - **Convenient Aliases** - Common shortcuts like `errors.server.db()` for frequent use cases
15
+ - **Built-in Middleware** - Ready-to-use error handlers for popular frameworks (Hono, and more coming)
16
+ - **Framework Agnostic** - Works with Express, Hono, Fastify, and any Node.js framework
17
+ - **Zero Dependencies** - Lightweight with no external dependencies (middleware have optional peer dependencies)
18
+ - **CommonJS** - Standard CommonJS for Node.js
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @fastify-utils/error
24
+ # or
25
+ pnpm add @fastify-utils/error
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```javascript
31
+ // CommonJS
32
+ const { errors } = require("@fastify-utils/error")
33
+
34
+ // or TypeScript / ES Modules
35
+ import { errors } from "@fastify-utils/error"
36
+
37
+ // Client errors
38
+ throw errors.client.authentication("Invalid token")
39
+ throw errors.client.notFound("User", "123")
40
+ throw errors.client.validation("Email is required")
41
+
42
+ // Server errors
43
+ throw errors.server.internal("Database connection failed")
44
+ throw errors.server.db("Connection pool exhausted")
45
+ ```
46
+
47
+ ## Available Errors
48
+
49
+ ### Client Errors (4xx)
50
+
51
+ 28 client error types covering all standard 4xx HTTP status codes:
52
+
53
+ | Method | Status | Description | Example |
54
+ | ----------------------------- | ------ | ---------------------------- | -------------------------------------------------------------------------- |
55
+ | `badRequest` | 400 | Malformed request | `errors.client.badRequest("Invalid JSON")` |
56
+ | `authentication` | 401 | Missing/invalid credentials | `errors.client.authentication("Token expired")` |
57
+ | `paymentRequired` | 402 | Payment required | `errors.client.paymentRequired("Subscription required")` |
58
+ | `authorization` | 403 | Insufficient permissions | `errors.client.authorization("Admin access required")` |
59
+ | `notFound` | 404 | Resource not found | `errors.client.notFound("User", "123")` |
60
+ | `methodNotAllowed` | 405 | HTTP method not allowed | `errors.client.methodNotAllowed("POST not allowed")` |
61
+ | `notAcceptable` | 406 | Not acceptable | `errors.client.notAcceptable("Only JSON supported")` |
62
+ | `proxyAuthenticationRequired` | 407 | Proxy auth required | `errors.client.proxyAuthenticationRequired("Proxy auth needed")` |
63
+ | `requestTimeout` | 408 | Request timeout | `errors.client.requestTimeout("Request took too long")` |
64
+ | `conflict` | 409 | Resource conflict | `errors.client.conflict("Email already exists")` |
65
+ | `gone` | 410 | Resource permanently deleted | `errors.client.gone("Resource permanently deleted")` |
66
+ | `lengthRequired` | 411 | Length header required | `errors.client.lengthRequired("Content-Length required")` |
67
+ | `preconditionFailed` | 412 | Precondition failed | `errors.client.preconditionFailed("ETag mismatch")` |
68
+ | `payloadTooLarge` | 413 | Payload too large | `errors.client.payloadTooLarge("File too large", 5000000)` |
69
+ | `uriTooLong` | 414 | URI too long | `errors.client.uriTooLong("URL exceeds maximum length")` |
70
+ | `unsupportedMediaType` | 415 | Unsupported media type | `errors.client.unsupportedMediaType("Only image/* allowed")` |
71
+ | `rangeNotSatisfiable` | 416 | Range not satisfiable | `errors.client.rangeNotSatisfiable("Invalid byte range")` |
72
+ | `expectationFailed` | 417 | Expectation failed | `errors.client.expectationFailed("Expect header failed")` |
73
+ | `misdirectedRequest` | 421 | Misdirected request | `errors.client.misdirectedRequest("Wrong server")` |
74
+ | `validation` | 422 | Invalid input data | `errors.client.validation("Invalid email", { email: ["Invalid format"] })` |
75
+ | `locked` | 423 | Resource locked | `errors.client.locked("Resource is locked")` |
76
+ | `failedDependency` | 424 | Failed dependency | `errors.client.failedDependency("Dependency failed")` |
77
+ | `tooEarly` | 425 | Too early | `errors.client.tooEarly("Request too early")` |
78
+ | `upgradeRequired` | 426 | Upgrade required | `errors.client.upgradeRequired("Upgrade to TLS required")` |
79
+ | `preconditionRequired` | 428 | Precondition required | `errors.client.preconditionRequired("If-Match header required")` |
80
+ | `rateLimit` | 429 | Too many requests | `errors.client.rateLimit("Rate limit exceeded", 60)` |
81
+ | `requestHeaderFieldsTooLarge` | 431 | Headers too large | `errors.client.requestHeaderFieldsTooLarge("Request headers too large")` |
82
+ | `unavailableForLegalReasons` | 451 | Legal restriction | `errors.client.unavailableForLegalReasons("Blocked by legal order")` |
83
+
84
+ ### Server Errors (5xx)
85
+
86
+ 11 server error types plus convenient aliases:
87
+
88
+ | Method | Status | Description | Example |
89
+ | ------------------------------- | ------ | ------------------------------- | -------------------------------------------------------------------- |
90
+ | `internal` | 500 | Internal server error | `errors.server.internal(caughtError)` |
91
+ | `notImplemented` | 501 | Feature not implemented | `errors.server.notImplemented("Feature not available")` |
92
+ | `badGateway` | 502 | External service error | `errors.server.badGateway(stripeError, "Stripe")` |
93
+ | `serviceUnavailable` | 503 | Service temporarily unavailable | `errors.server.serviceUnavailable("Maintenance mode", 60)` |
94
+ | `gatewayTimeout` | 504 | External service timeout | `errors.server.gatewayTimeout("Payment timeout", "Stripe")` |
95
+ | `httpVersionNotSupported` | 505 | HTTP version not supported | `errors.server.httpVersionNotSupported("HTTP/2 required")` |
96
+ | `variantAlsoNegotiates` | 506 | Variant also negotiates | `errors.server.variantAlsoNegotiates("Configuration error")` |
97
+ | `insufficientStorage` | 507 | Insufficient storage | `errors.server.insufficientStorage("Out of storage space")` |
98
+ | `loopDetected` | 508 | Loop detected | `errors.server.loopDetected("Circular dependency detected")` |
99
+ | `notExtended` | 510 | Not extended | `errors.server.notExtended("Extension not supported")` |
100
+ | `networkAuthenticationRequired` | 511 | Network authentication required | `errors.server.networkAuthenticationRequired("Proxy auth required")` |
101
+
102
+ **Convenient Aliases:**
103
+
104
+ Common shortcuts for frequent use cases:
105
+
106
+ | Alias | Maps To | Status | Example |
107
+ | -------------------- | --------------------- | ------ | ----------------------------------------------------------------- |
108
+ | **Client Aliases** | | | |
109
+ | `validate` | `validation` | 422 | `errors.client.validate("Invalid email format")` |
110
+ | `permission` | `authorization` | 403 | `errors.client.permission("Access denied")` |
111
+ | `access` | `authorization` | 403 | `errors.client.access("Insufficient permissions")` |
112
+ | `idNotFound` | `notFound` | 404 | `errors.client.idNotFound("User", "123")` |
113
+ | `duplicate` | `conflict` | 409 | `errors.client.duplicate("Email already exists")` |
114
+ | `thirdParty` | `failedDependency` | 424 | `errors.client.thirdParty("External service failed")` |
115
+ | **Server Aliases** | | | |
116
+ | `db` | `serviceUnavailable` | 503 | `errors.server.db("Connection pool exhausted")` |
117
+ | `fetch` | `badGateway` | 502 | `errors.server.fetch("GitHub API unreachable")` |
118
+ | `envNotSet` | `notImplemented` | 501 | `errors.server.envNotSet("DATABASE_URL not configured")` |
119
+ | `envInvalid` | `notImplemented` | 501 | `errors.server.envInvalid("DATABASE_URL must be a valid URL")` |
120
+ | `maintenance` | `serviceUnavailable` | 503 | `errors.server.maintenance("System under maintenance")` |
121
+ | `migration` | `insufficientStorage` | 507 | `errors.server.migration("Migration storage limit exceeded")` |
122
+ | `unhandledRejection` | `internal` | 500 | `errors.server.unhandledRejection("Unhandled promise rejection")` |
123
+ | `uncaughtException` | `internal` | 500 | `errors.server.uncaughtException("Uncaught exception")` |
124
+
125
+ ## Usage Examples
126
+
127
+ ### Basic Error Throwing
128
+
129
+ ```typescript
130
+ import { errors } from "@fastify-utils/error"
131
+
132
+ if (!user) {
133
+ throw errors.client.notFound("User", userId)
134
+ }
135
+
136
+ if (!hasPermission) {
137
+ throw errors.client.authorization("Admin access required")
138
+ }
139
+ ```
140
+
141
+ ### Auto-Normalization
142
+
143
+ The package automatically normalizes any value to a string:
144
+
145
+ ```typescript
146
+ import { errors } from "@fastify-utils/error"
147
+
148
+ try {
149
+ await externalAPI.call()
150
+ } catch (err) {
151
+ throw errors.server.badGateway(err, "External API")
152
+ }
153
+ ```
154
+
155
+ ### Validation Errors
156
+
157
+ ```typescript
158
+ import { errors } from "@fastify-utils/error"
159
+
160
+ const validationErrors = {
161
+ email: ["Invalid email format", "Email already exists"],
162
+ password: ["Password too weak"]
163
+ }
164
+
165
+ throw errors.client.validation("Validation failed", validationErrors)
166
+ ```
167
+
168
+ ### NotFound with Auto-formatting
169
+
170
+ ```typescript
171
+ import { errors } from "@fastify-utils/error"
172
+
173
+ // Auto-formatted message: "User 123 not found"
174
+ throw errors.client.notFound("User", "123")
175
+
176
+ // Custom message
177
+ throw errors.client.notFound("Custom message: User not found in database")
178
+ ```
179
+
180
+ ### Using Convenient Aliases
181
+
182
+ ```typescript
183
+ import { errors } from "@fastify-utils/error"
184
+
185
+ // Validation errors
186
+ if (!email.includes("@")) {
187
+ throw errors.client.validate("Invalid email format")
188
+ }
189
+
190
+ // Permission checks
191
+ if (!user.isAdmin) {
192
+ throw errors.client.permission("Admin access required")
193
+ }
194
+
195
+ // ID lookups
196
+ const user = await db.findUser(userId)
197
+ if (!user) {
198
+ throw errors.client.idNotFound("User", userId)
199
+ }
200
+
201
+ // Duplicate entries
202
+ if (await db.emailExists(email)) {
203
+ throw errors.client.duplicate("User with this email already exists")
204
+ }
205
+
206
+ // Database errors
207
+ try {
208
+ await db.query("SELECT * FROM users")
209
+ } catch (err) {
210
+ throw errors.server.db(err)
211
+ }
212
+
213
+ // External API failures
214
+ try {
215
+ await fetch("https://api.github.com/users/octocat")
216
+ } catch (err) {
217
+ throw errors.server.fetch(err)
218
+ }
219
+
220
+ // Third-party integrations
221
+ try {
222
+ await stripe.customers.create({ email })
223
+ } catch (err) {
224
+ throw errors.client.thirdParty(err)
225
+ }
226
+
227
+ // Environment configuration - missing
228
+ if (!process.env.DATABASE_URL) {
229
+ throw errors.server.envNotSet("DATABASE_URL environment variable not set")
230
+ }
231
+
232
+ // Environment configuration - invalid value
233
+ if (process.env.NODE_ENV && !["development", "production", "test"].includes(process.env.NODE_ENV)) {
234
+ throw errors.server.envInvalid("NODE_ENV must be development, production, or test")
235
+ }
236
+
237
+ // Maintenance mode
238
+ if (isMaintenanceMode) {
239
+ throw errors.server.maintenance("System is under scheduled maintenance", 3600)
240
+ }
241
+
242
+ // Migration failures
243
+ try {
244
+ await runMigration()
245
+ } catch (err) {
246
+ throw errors.server.migration(err)
247
+ }
248
+
249
+ // Node.js process error handlers
250
+ process.on("unhandledRejection", (reason) => {
251
+ console.error(errors.server.unhandledRejection(reason))
252
+ process.exit(1)
253
+ })
254
+
255
+ process.on("uncaughtException", (error) => {
256
+ console.error(errors.server.uncaughtException(error))
257
+ process.exit(1)
258
+ })
259
+ ```
260
+
261
+ ### Additional Utilities
262
+
263
+ ```typescript
264
+ import { errors, isValidRFC9457Json } from "@fastify-utils/error"
265
+
266
+ // Create error by status code
267
+ throw errors.byStatus(404, "Not found")
268
+
269
+ // Get JSON without throwing
270
+ const json = errors.client.badRequest("Invalid input").toJSON()
271
+
272
+ // Validate RFC 9457 response
273
+ if (isValidRFC9457Json(data)) {
274
+ // Valid RFC 9457 error
275
+ }
276
+ ```
277
+
278
+ ## Framework Integration
279
+
280
+ ### Fastify
281
+
282
+ ```typescript
283
+ import Fastify from "fastify"
284
+ import { errors, isHttpError } from "@fastify-utils/error"
285
+
286
+ const fastify = Fastify()
287
+
288
+ fastify.get("/users/:id", async (request, reply) => {
289
+ const user = await db.users.findById(request.params.id)
290
+
291
+ if (!user) {
292
+ throw errors.client.notFound("User", request.params.id)
293
+ }
294
+
295
+ return user
296
+ })
297
+
298
+ fastify.setErrorHandler((error, request, reply) => {
299
+ if (isHttpError(error)) {
300
+ return reply.status(error.status).send(error.toJSON())
301
+ }
302
+
303
+ const internalError = errors.server.internal(error)
304
+ reply.status(500).send(internalError.toJSON())
305
+ })
306
+ ```
307
+
308
+ ### Express
309
+
310
+ ```typescript
311
+ import express from "express"
312
+ import { errors, isHttpError } from "@fastify-utils/error"
313
+
314
+ const app = express()
315
+
316
+ app.get("/users/:id", async (req, res) => {
317
+ const user = await db.users.findById(req.params.id)
318
+
319
+ if (!user) {
320
+ throw errors.client.notFound("User", req.params.id)
321
+ }
322
+
323
+ res.json(user)
324
+ })
325
+
326
+ app.use((err, req, res, next) => {
327
+ if (isHttpError(err)) {
328
+ return res.status(err.status).json(err.toJSON())
329
+ }
330
+
331
+ const internalError = errors.server.internal(err)
332
+ res.status(500).json(internalError.toJSON())
333
+ })
334
+ ```
335
+
336
+ ## Configuration
337
+
338
+ Set the base URL for error type URIs using the environment variable:
339
+
340
+ ```bash
341
+ export RFC9457_BASE_URL=https://api.example.com/errors
342
+ ```
343
+
344
+ ## Error Response Format
345
+
346
+ All errors follow RFC 9457 structure:
347
+
348
+ ```json
349
+ {
350
+ "type": "about:blank#not-found",
351
+ "title": "Not Found",
352
+ "status": 404,
353
+ "detail": "User 123 not found"
354
+ }
355
+ ```
356
+
357
+ With custom base URL:
358
+
359
+ ```json
360
+ {
361
+ "type": "https://api.example.com/errors/validation",
362
+ "title": "Validation Error",
363
+ "status": 422,
364
+ "detail": "Validation failed",
365
+ "validationErrors": {
366
+ "email": ["Invalid email format"]
367
+ }
368
+ }
369
+ ```
370
+
371
+ ## TypeScript Support
372
+
373
+ Full type safety and IDE autocomplete:
374
+
375
+ ```typescript
376
+ import { errors, isHttpError } from "@fastify-utils/error"
377
+
378
+ const err = errors.client.validation("Invalid data")
379
+
380
+ if (isHttpError(err)) {
381
+ console.log(err.status) // 422
382
+ console.log(err.toJSON()) // { type: "...", title: "...", ... }
383
+ }
384
+ ```
385
+
386
+ ## API Reference
387
+
388
+ ### Categorized Errors (Recommended)
389
+
390
+ ```typescript
391
+ import { errors } from "@fastify-utils/error"
392
+
393
+ throw errors.client.badRequest("Invalid input")
394
+ throw errors.server.internal("System error")
395
+ ```
396
+
397
+ ### Flat API (Alternative)
398
+
399
+ ```typescript
400
+ import { error } from "@fastify-utils/error"
401
+
402
+ throw error.badRequest("Invalid input")
403
+ throw error.internal("System error")
404
+ ```
405
+
406
+ ### Helpers
407
+
408
+ ```typescript
409
+ import { isHttpError } from "@fastify-utils/error"
410
+
411
+ if (isHttpError(err)) {
412
+ console.log(err.status)
413
+ console.log(err.toJSON())
414
+ }
415
+ ```
416
+
417
+ ## Development
418
+
419
+ ```bash
420
+ pnpm install
421
+ node --run build
422
+ node --run lint
423
+ ```
424
+
425
+ ## License
426
+
427
+ MIT — see [LICENSE](LICENSE) for details.
428
+
429
+ ## Links
430
+
431
+ | Resource | URL |
432
+ | ----------- | --------------------------------------------- |
433
+ | GitHub | https://github.com/xcfio/fastify-utils |
434
+ | npm | https://www.npmjs.com/package/fastify-utils |
435
+ | Bug reports | https://github.com/xcfio/fastify-utils/issues |
436
+ | Help | https://dsc.gg/xcfio |
437
+
438
+ ---
439
+
440
+ Made with ❤️ by [xcfio](https://github.com/xcfio)