@nextrush/errors 3.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tanzim (NextRush)
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.
package/README.md ADDED
@@ -0,0 +1,681 @@
1
+ # @nextrush/errors
2
+
3
+ > Standardized HTTP error handling that eliminates response inconsistency and builds API client trust.
4
+
5
+ ## The Problem
6
+
7
+ Every API returns errors differently. This creates chaos for both developers and API consumers:
8
+
9
+ **Inconsistent error responses** plague every backend project. One endpoint returns `{error: "..."}`, another returns `{message: "..."}`, and a third leaks stack traces in production. API clients can't reliably handle errors because there's no standard format.
10
+
11
+ **Internal errors leak to users.** A database connection timeout becomes `PostgreSQL connection failed on pool 'primary'` in the API response. Security researchers see your infrastructure. Users see confusing technical jargon instead of actionable messages.
12
+
13
+ **Manual error formatting is tedious.** Every route handler manually sets status codes, constructs JSON responses, and decides what to expose. Copy-paste error handling leads to bugs. Forgetting `try-catch` crashes the server.
14
+
15
+ **No programmatic error handling.** API clients resort to parsing error messages with regex because there are no stable error codes. A typo in an error message breaks production integrations.
16
+
17
+ ## How NextRush Approaches This
18
+
19
+ NextRush treats **errors as API contracts**, not exceptions.
20
+
21
+ Every error has three responsibilities:
22
+
23
+ 1. **HTTP status code** - Semantic meaning for browsers and clients
24
+ 2. **Human message** - Clear explanation for developers/users
25
+ 3. **Machine code** - Stable identifier for programmatic handling
26
+
27
+ The framework distinguishes between **client-safe errors** (4xx) and **server-internal errors** (5xx) with an `expose` flag. Client errors show detailed messages. Server errors hide implementation details by default.
28
+
29
+ All errors serialize to a consistent JSON format automatically. No manual response formatting. No leaked stack traces. No security risks.
30
+
31
+ ## Mental Model
32
+
33
+ Think of errors as **structured API responses**, not crashes.
34
+
35
+ ### Errors Are Contracts
36
+
37
+ ```
38
+ User Request → Handler Logic → Error Thrown → Middleware Catches → JSON Response
39
+ ```
40
+
41
+ When you throw `NotFoundError`, you're declaring an API contract:
42
+
43
+ - **Status:** 404 Not Found
44
+ - **Code:** `NOT_FOUND`
45
+ - **Message:** Custom message you provide
46
+ - **Format:** Consistent JSON structure
47
+
48
+ ### The Expose Flag
49
+
50
+ Every error has an `expose` flag that acts as a **privacy boundary**:
51
+
52
+ ```typescript
53
+ // Client errors (4xx): expose = true by default
54
+ throw new NotFoundError('User #123 not found');
55
+ // → Client sees: {"message": "User #123 not found", "code": "NOT_FOUND"}
56
+
57
+ // Server errors (5xx): expose = false by default
58
+ throw new InternalServerError('Redis connection timeout');
59
+ // → Client sees: {"message": "Internal Server Error", "code": "INTERNAL_ERROR"}
60
+ // → Server logs: Full error with stack trace
61
+ ```
62
+
63
+ This prevents security leaks while maintaining debuggability.
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pnpm add @nextrush/errors
69
+ ```
70
+
71
+ ## Quick Start
72
+
73
+ ```typescript
74
+ import { createApp } from '@nextrush/core';
75
+ import { errorHandler, NotFoundError, BadRequestError } from '@nextrush/errors';
76
+
77
+ const app = createApp();
78
+
79
+ // Add error handling middleware FIRST
80
+ app.use(errorHandler());
81
+
82
+ app.get('/users/:id', (ctx) => {
83
+ const user = users.get(ctx.params.id);
84
+ if (!user) {
85
+ throw new NotFoundError('User not found');
86
+ }
87
+ ctx.json(user);
88
+ });
89
+
90
+ app.post('/users', (ctx) => {
91
+ if (!ctx.body.email) {
92
+ throw new BadRequestError('Email is required');
93
+ }
94
+ // Create user...
95
+ ctx.json({ success: true });
96
+ });
97
+
98
+ // Request: GET /users/999
99
+ // Response: 404 Not Found
100
+ // {
101
+ // "error": "NotFoundError",
102
+ // "message": "User not found",
103
+ // "code": "NOT_FOUND",
104
+ // "status": 404
105
+ // }
106
+
107
+ // Request: POST /users (no email)
108
+ // Response: 400 Bad Request
109
+ // {
110
+ // "error": "BadRequestError",
111
+ // "message": "Email is required",
112
+ // "code": "BAD_REQUEST",
113
+ // "status": 400
114
+ // }
115
+ ```
116
+
117
+ ## What NextRush Does Automatically
118
+
119
+ When you throw an `HttpError` with error middleware enabled:
120
+
121
+ 1. **Catches the error** - No uncaught exceptions crash your server
122
+ 2. **Sets HTTP status** - Correct status code from error class
123
+ 3. **Formats JSON response** - Consistent `{error, message, code, status}` structure
124
+ 4. **Applies expose flag** - Hides sensitive 5xx details, shows 4xx details
125
+ 5. **Logs appropriately** - 5xx logged as errors, 4xx as warnings
126
+ 6. **Preserves stack traces** - Full debugging in development, hidden in production
127
+
128
+ You don't write error handling code. You **declare error states** and NextRush handles the rest.
129
+
130
+ ## Features
131
+
132
+ - **Type-Safe Errors**: Full TypeScript support with proper error inheritance
133
+ - **HTTP Status Codes**: All standard 4xx and 5xx errors included
134
+ - **Validation Errors**: Built-in validation error types
135
+ - **Factory Functions**: Quick error creation with `badRequest()`, `notFound()`, etc.
136
+ - **Error Middleware**: Automatic error response formatting
137
+ - **Error Codes**: Custom error codes for API consumers
138
+
139
+ ## HTTP Errors
140
+
141
+ ### 4xx Client Errors
142
+
143
+ ```typescript
144
+ import {
145
+ BadRequestError, // 400
146
+ UnauthorizedError, // 401
147
+ PaymentRequiredError, // 402
148
+ ForbiddenError, // 403
149
+ NotFoundError, // 404
150
+ MethodNotAllowedError, // 405
151
+ NotAcceptableError, // 406
152
+ RequestTimeoutError, // 408
153
+ ConflictError, // 409
154
+ GoneError, // 410
155
+ LengthRequiredError, // 411
156
+ PreconditionFailedError, // 412
157
+ PayloadTooLargeError, // 413
158
+ UriTooLongError, // 414
159
+ UnsupportedMediaTypeError, // 415
160
+ RangeNotSatisfiableError, // 416
161
+ ExpectationFailedError, // 417
162
+ ImATeapotError, // 418
163
+ UnprocessableEntityError, // 422
164
+ LockedError, // 423
165
+ FailedDependencyError, // 424
166
+ TooEarlyError, // 425
167
+ UpgradeRequiredError, // 426
168
+ PreconditionRequiredError, // 428
169
+ TooManyRequestsError, // 429
170
+ RequestHeaderFieldsTooLargeError, // 431
171
+ UnavailableForLegalReasonsError, // 451
172
+ } from '@nextrush/errors';
173
+ ```
174
+
175
+ ### 5xx Server Errors
176
+
177
+ ```typescript
178
+ import {
179
+ InternalServerError, // 500
180
+ NotImplementedError, // 501
181
+ BadGatewayError, // 502
182
+ ServiceUnavailableError, // 503
183
+ GatewayTimeoutError, // 504
184
+ HttpVersionNotSupportedError, // 505
185
+ VariantAlsoNegotiatesError, // 506
186
+ InsufficientStorageError, // 507
187
+ LoopDetectedError, // 508
188
+ NotExtendedError, // 510
189
+ NetworkAuthRequiredError, // 511
190
+ } from '@nextrush/errors';
191
+ ```
192
+
193
+ ## Usage Examples
194
+
195
+ ### Basic Usage
196
+
197
+ ```typescript
198
+ // Simple message
199
+ throw new NotFoundError('User not found');
200
+
201
+ // With error code
202
+ throw new BadRequestError('Invalid email format', {
203
+ code: 'INVALID_EMAIL',
204
+ });
205
+
206
+ // With additional details
207
+ throw new UnprocessableEntityError('Validation failed', {
208
+ code: 'VALIDATION_ERROR',
209
+ details: {
210
+ email: 'Invalid format',
211
+ age: 'Must be positive',
212
+ },
213
+ });
214
+ ```
215
+
216
+ ### With Error Codes
217
+
218
+ ```typescript
219
+ throw new UnauthorizedError('Token expired', {
220
+ code: 'TOKEN_EXPIRED',
221
+ expose: true,
222
+ });
223
+
224
+ // API response:
225
+ // {
226
+ // "error": "Token expired",
227
+ // "code": "TOKEN_EXPIRED",
228
+ // "status": 401
229
+ // }
230
+ ```
231
+
232
+ ## Factory Functions
233
+
234
+ Create errors with less boilerplate:
235
+
236
+ ```typescript
237
+ import {
238
+ badRequest,
239
+ unauthorized,
240
+ forbidden,
241
+ notFound,
242
+ methodNotAllowed,
243
+ conflict,
244
+ unprocessableEntity,
245
+ tooManyRequests,
246
+ internalError,
247
+ serviceUnavailable,
248
+ badGateway,
249
+ gatewayTimeout,
250
+ createError,
251
+ } from '@nextrush/errors';
252
+
253
+ // Quick creation
254
+ throw badRequest('Invalid input');
255
+ throw notFound('Resource not found');
256
+ throw unauthorized('Please login');
257
+
258
+ // With options
259
+ throw tooManyRequests('Rate limit exceeded', {
260
+ code: 'RATE_LIMIT',
261
+ details: { retryAfter: 60 },
262
+ });
263
+
264
+ // Custom status code
265
+ throw createError(418, "I'm a teapot");
266
+ ```
267
+
268
+ ## Validation Errors
269
+
270
+ Specialized errors for input validation:
271
+
272
+ ```typescript
273
+ import {
274
+ ValidationError,
275
+ RequiredFieldError,
276
+ TypeMismatchError,
277
+ RangeValidationError,
278
+ LengthError,
279
+ PatternError,
280
+ InvalidEmailError,
281
+ InvalidUrlError,
282
+ } from '@nextrush/errors';
283
+
284
+ // Generic validation error with issues
285
+ throw new ValidationError([
286
+ { path: 'email', message: 'Required', rule: 'required' },
287
+ { path: 'age', message: 'Must be positive', rule: 'range' },
288
+ ]);
289
+
290
+ // Static factory methods
291
+ throw ValidationError.fromField('email', 'Invalid format', 'email');
292
+ throw ValidationError.fromFields({ email: 'Required', age: 'Must be number' });
293
+
294
+ // Specific field errors
295
+ throw new RequiredFieldError('email');
296
+ throw new TypeMismatchError('age', 'number', 'string');
297
+ throw new RangeValidationError('age', 18, 100);
298
+ throw new LengthError('password', 8, 128);
299
+ throw new PatternError('username', '^[a-z0-9]+$');
300
+ throw new InvalidEmailError('email');
301
+ throw new InvalidUrlError('website');
302
+ ```
303
+
304
+ ## Error Middleware
305
+
306
+ ### errorHandler(options?)
307
+
308
+ Format errors as JSON responses:
309
+
310
+ ```typescript
311
+ import { errorHandler } from '@nextrush/errors';
312
+
313
+ app.use(
314
+ errorHandler({
315
+ // Include stack trace (default: false)
316
+ includeStack: process.env.NODE_ENV !== 'production',
317
+
318
+ // Custom error logger (default: logs 5xx as errors, 4xx as warnings)
319
+ logger: (err, ctx) => {
320
+ myLogger.error(err, { path: ctx.path });
321
+ },
322
+
323
+ // Custom error response transformer
324
+ transform: (err, ctx) => ({
325
+ error: err.message,
326
+ status: ctx.status,
327
+ }),
328
+ })
329
+ );
330
+ ```
331
+
332
+ ### notFoundHandler()
333
+
334
+ Catch-all 404 handler:
335
+
336
+ ```typescript
337
+ import { notFoundHandler } from '@nextrush/errors';
338
+
339
+ // Add after all routes
340
+ app.use(notFoundHandler());
341
+
342
+ // Custom message
343
+ app.use(notFoundHandler('Route not found'));
344
+ ```
345
+
346
+ ### catchAsync(fn)
347
+
348
+ Wrap async handlers to catch errors:
349
+
350
+ ```typescript
351
+ import { catchAsync } from '@nextrush/errors';
352
+
353
+ app.get(
354
+ '/users/:id',
355
+ catchAsync(async (ctx) => {
356
+ const user = await db.findUser(ctx.params.id);
357
+ if (!user) throw new NotFoundError('User not found');
358
+ ctx.json(user);
359
+ })
360
+ );
361
+ ```
362
+
363
+ ## Error Utilities
364
+
365
+ ```typescript
366
+ import { isHttpError, getErrorStatus, getSafeErrorMessage } from '@nextrush/errors';
367
+
368
+ // Check if error is HTTP error
369
+ if (isHttpError(err)) {
370
+ console.log(err.status, err.code);
371
+ }
372
+
373
+ // Get status from any error
374
+ const status = getErrorStatus(err); // Returns 500 for unknown errors
375
+
376
+ // Get safe message (hides internal errors)
377
+ const message = getSafeErrorMessage(err);
378
+ // Returns generic message for 500 errors in production
379
+ ```
380
+
381
+ ## Base Classes
382
+
383
+ ### HttpError
384
+
385
+ Base class for all HTTP errors:
386
+
387
+ ```typescript
388
+ import { HttpError } from '@nextrush/errors';
389
+
390
+ class CustomError extends HttpError {
391
+ constructor(message: string) {
392
+ super(422, message, { code: 'CUSTOM_ERROR' });
393
+ }
394
+ }
395
+ ```
396
+
397
+ ### NextRushError
398
+
399
+ Base class for framework errors:
400
+
401
+ ```typescript
402
+ import { NextRushError } from '@nextrush/errors';
403
+
404
+ class ConfigError extends NextRushError {
405
+ constructor(message: string) {
406
+ super(message, { code: 'CONFIG_ERROR' });
407
+ }
408
+ }
409
+ ```
410
+
411
+ ## API Reference
412
+
413
+ ### Exports
414
+
415
+ ```typescript
416
+ import {
417
+ // Base classes
418
+ HttpError,
419
+ NextRushError,
420
+
421
+ // 4xx errors
422
+ BadRequestError,
423
+ UnauthorizedError,
424
+ ForbiddenError,
425
+ NotFoundError,
426
+ // ... all 4xx errors
427
+
428
+ // 5xx errors
429
+ InternalServerError,
430
+ ServiceUnavailableError,
431
+ // ... all 5xx errors
432
+
433
+ // Validation errors
434
+ ValidationError,
435
+ RequiredFieldError,
436
+ TypeMismatchError,
437
+ // ... all validation errors
438
+
439
+ // Factory functions
440
+ badRequest,
441
+ unauthorized,
442
+ notFound,
443
+ createError,
444
+ // ... all factory functions
445
+
446
+ // Middleware
447
+ errorHandler,
448
+ notFoundHandler,
449
+ catchAsync,
450
+
451
+ // Utilities
452
+ isHttpError,
453
+ getErrorStatus,
454
+ getSafeErrorMessage,
455
+ } from '@nextrush/errors';
456
+ ```
457
+
458
+ ### Types
459
+
460
+ ```typescript
461
+ import type {
462
+ HttpErrorOptions,
463
+ ValidationIssue,
464
+ ErrorContext,
465
+ ErrorHandlerOptions,
466
+ ErrorMiddleware,
467
+ } from '@nextrush/errors';
468
+
469
+ interface HttpErrorOptions {
470
+ code?: string;
471
+ expose?: boolean;
472
+ details?: Record<string, unknown>;
473
+ cause?: unknown;
474
+ }
475
+
476
+ interface ValidationIssue {
477
+ path: string;
478
+ message: string;
479
+ rule?: string;
480
+ expected?: unknown;
481
+ received?: unknown;
482
+ }
483
+ ```
484
+
485
+ ````
486
+
487
+ ## Common Mistakes
488
+
489
+ ### Mistake 1: Using Generic Errors for Specific Cases
490
+
491
+ ```typescript
492
+ // ❌ Don't do this
493
+ throw new Error('User not found');
494
+ // → Returns 500 Internal Server Error, no error code
495
+
496
+ // ✅ Do this instead
497
+ throw new NotFoundError('User not found');
498
+ // → Returns 404 Not Found with NOT_FOUND code
499
+ ````
500
+
501
+ **Why it's wrong:** Generic JavaScript `Error` becomes 500 Internal Server Error. The client can't distinguish between "not found" and "server crash".
502
+
503
+ ### Mistake 2: Exposing Internal Implementation Details
504
+
505
+ ```typescript
506
+ // ❌ Don't do this
507
+ throw new InternalServerError('PostgreSQL connection timeout on pool "primary"', {
508
+ expose: true, // Leaks infrastructure details!
509
+ });
510
+
511
+ // ✅ Do this instead
512
+ throw new InternalServerError('Database temporarily unavailable');
513
+ // expose defaults to false for 5xx errors
514
+ // Full error logged server-side for debugging
515
+ ```
516
+
517
+ **Why it's wrong:** Exposing database names, connection pools, or internal service names helps attackers understand your infrastructure.
518
+
519
+ ### Mistake 3: Forgetting Error Middleware
520
+
521
+ ```typescript
522
+ // ❌ Errors won't be formatted
523
+ const app = createApp();
524
+
525
+ app.get('/users', (ctx) => {
526
+ throw new NotFoundError('User not found'); // Crashes or returns HTML error page!
527
+ });
528
+
529
+ // ✅ Add errorHandler BEFORE routes
530
+ const app = createApp();
531
+ app.use(errorHandler()); // This catches and formats errors
532
+
533
+ app.get('/users', (ctx) => {
534
+ throw new NotFoundError('User not found'); // Returns proper JSON
535
+ });
536
+ ```
537
+
538
+ ### Mistake 4: Using Errors for Control Flow
539
+
540
+ ```typescript
541
+ // ❌ Don't use errors for expected business logic
542
+ async function getUser(id: string): Promise<User> {
543
+ const user = await db.findUser(id);
544
+ if (!user) throw new NotFoundError(); // Too expensive for expected case
545
+ return user;
546
+ }
547
+
548
+ // ✅ Use nullable returns for expected cases
549
+ async function getUser(id: string): Promise<User | null> {
550
+ return await db.findUser(id);
551
+ }
552
+
553
+ // ✅ Throw errors at the HTTP boundary
554
+ app.get('/users/:id', async (ctx) => {
555
+ const user = await getUser(ctx.params.id);
556
+ if (!user) throw new NotFoundError('User not found');
557
+ ctx.json(user);
558
+ });
559
+ ```
560
+
561
+ **Why it's wrong:** Throwing errors for control flow is expensive (stack trace construction) and makes code harder to reason about.
562
+
563
+ ### Mistake 5: Missing Error Codes for API Clients
564
+
565
+ ```typescript
566
+ // ❌ Clients can't handle errors programmatically
567
+ throw new BadRequestError('Invalid email format');
568
+ // Response: {"message": "Invalid email format"} // No code!
569
+
570
+ // ✅ Include error codes
571
+ throw new BadRequestError('Invalid email format', {
572
+ code: 'INVALID_EMAIL',
573
+ });
574
+ // Response: {"message": "Invalid email format", "code": "INVALID_EMAIL"}
575
+
576
+ // Client can now:
577
+ if (error.code === 'INVALID_EMAIL') {
578
+ // Show email field error
579
+ }
580
+ ```
581
+
582
+ ## When NOT to Use
583
+
584
+ ### Don't Use for Validation in Reusable Libraries
585
+
586
+ If you're building a reusable library (not an HTTP handler), return validation results instead of throwing:
587
+
588
+ ```typescript
589
+ // ❌ Don't throw in library code
590
+ export function validateEmail(email: string): string {
591
+ if (!isValid(email)) throw new InvalidEmailError(); // Caller can't control behavior
592
+ return email;
593
+ }
594
+
595
+ // ✅ Return validation result
596
+ export function validateEmail(email: string): { valid: boolean; error?: string } {
597
+ return isValid(email) ? { valid: true } : { valid: false, error: 'Invalid email format' };
598
+ }
599
+
600
+ // ✅ Throw at the HTTP boundary
601
+ app.post('/users', (ctx) => {
602
+ const result = validateEmail(ctx.body.email);
603
+ if (!result.valid) {
604
+ throw new BadRequestError(result.error!, { code: 'INVALID_EMAIL' });
605
+ }
606
+ });
607
+ ```
608
+
609
+ **Why:** Libraries should be composable. Throwing errors forces a specific error handling strategy on consumers.
610
+
611
+ ### Don't Use for Expected Empty Results
612
+
613
+ ```typescript
614
+ // ❌ Don't throw for queries that might return nothing
615
+ async function searchUsers(query: string): Promise<User[]> {
616
+ const users = await db.search(query);
617
+ if (users.length === 0) throw new NotFoundError(); // Expected case!
618
+ return users;
619
+ }
620
+
621
+ // ✅ Return empty arrays for "no results"
622
+ async function searchUsers(query: string): Promise<User[]> {
623
+ return await db.search(query); // Empty array is valid
624
+ }
625
+
626
+ // ✅ Only throw when the resource *should* exist
627
+ app.get('/users/:id', async (ctx) => {
628
+ const user = await db.getById(ctx.params.id);
629
+ if (!user) throw new NotFoundError(); // Specific ID should exist
630
+ ctx.json(user);
631
+ });
632
+ ```
633
+
634
+ ### Don't Use for Non-HTTP Contexts
635
+
636
+ ```typescript
637
+ // ❌ Don't use HTTP errors in background jobs
638
+ async function processQueue() {
639
+ const job = await queue.pop();
640
+ if (!job) throw new NotFoundError(); // Wrong abstraction!
641
+ }
642
+
643
+ // ✅ Use domain-specific errors or return values
644
+ async function processQueue() {
645
+ const job = await queue.pop();
646
+ if (!job) return { processed: false, reason: 'queue_empty' };
647
+ // Process job...
648
+ return { processed: true };
649
+ }
650
+ ```
651
+
652
+ ## Runtime Compatibility
653
+
654
+ Works on all NextRush-supported runtimes:
655
+
656
+ | Runtime | Supported | Notes |
657
+ | ------------------- | --------- | ------------ |
658
+ | Node.js 20+ | ✅ | Full support |
659
+ | Bun 1.0+ | ✅ | Full support |
660
+ | Deno 2.0+ | ✅ | Full support |
661
+ | Cloudflare Workers | ✅ | Full support |
662
+ | Vercel Edge Runtime | ✅ | Full support |
663
+
664
+ **Zero external dependencies.** Uses only standard JavaScript `Error` APIs and NextRush types.
665
+
666
+ ## Best Practices
667
+
668
+ 1. **Use specific errors**: `NotFoundError` over generic `HttpError`
669
+ 2. **Include error codes**: Help API consumers handle errors programmatically
670
+ 3. **Don't expose internals**: Keep `expose: false` for 5xx errors (default)
671
+ 4. **Add error middleware first**: Before all routes
672
+ 5. **Validate early**: Throw validation errors at the start of handlers
673
+ 6. **Return nulls for expected "not found"**: Throw errors only at HTTP boundaries
674
+
675
+ ## License
676
+
677
+ MIT
678
+
679
+ ```
680
+
681
+ ```