@periodic/obsidian 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.
- package/LICENSE +21 -0
- package/README.md +858 -0
- package/dist/index.d.mts +162 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.js +424 -0
- package/dist/index.mjs +391 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
# โซ Periodic Obsidian
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@periodic/obsidian)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
**Production-grade HTTP error handling library for Express.js with TypeScript support**
|
|
8
|
+
|
|
9
|
+
Part of the **Periodic** series of Node.js middleware packages by Uday Thakur.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ๐ก Why Obsidian?
|
|
14
|
+
|
|
15
|
+
**Obsidian** gets its name from the volcanic glass known for its sharp edges and clarity โ just like how this library provides **sharp, clear error handling** for your APIs.
|
|
16
|
+
|
|
17
|
+
In geology, obsidian forms when lava cools rapidly, creating a material that's both beautiful and functional. Similarly, **@periodic/obsidian** was crafted through rapid iteration and real-world production experience to create something that's both elegant and practical.
|
|
18
|
+
|
|
19
|
+
The name represents:
|
|
20
|
+
- **Clarity**: Crystal-clear error messages and consistent structure
|
|
21
|
+
- **Sharpness**: Precise, type-safe error handling
|
|
22
|
+
- **Durability**: Production-tested and built to last
|
|
23
|
+
- **Natural**: Feels like a native part of your Express app
|
|
24
|
+
|
|
25
|
+
Just as ancient civilizations used obsidian for tools and weapons, modern developers can use **@periodic/obsidian** as their essential tool for building robust, production-ready APIs.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ๐ฏ Why Choose Obsidian?
|
|
30
|
+
|
|
31
|
+
Building robust APIs requires consistent, type-safe error handling, but most solutions come with significant challenges:
|
|
32
|
+
|
|
33
|
+
- **Generic error packages** lack framework integration
|
|
34
|
+
- **Built-in solutions** don't provide enough structure
|
|
35
|
+
- **Custom implementations** lead to inconsistent error responses
|
|
36
|
+
- **Missing TypeScript support** causes runtime errors
|
|
37
|
+
|
|
38
|
+
**Periodic Obsidian** provides the perfect solution:
|
|
39
|
+
|
|
40
|
+
โ
**60+ HTTP status code factories** for every standard status (100-511)
|
|
41
|
+
โ
**Framework-agnostic core** with clean Express adapter
|
|
42
|
+
โ
**TypeScript-first** with complete type safety
|
|
43
|
+
โ
**Zero runtime dependencies for the core.** Express is a peer dependency used only by the Express adapter.
|
|
44
|
+
โ
**Clean JSON serialization** - no stack traces in production
|
|
45
|
+
โ
**Flexible error metadata** - codes, details, and custom fields
|
|
46
|
+
โ
**Express middleware included** for automatic error handling
|
|
47
|
+
โ
**Designed for production use** with a stable and predictable API.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ๐ฆ Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install @periodic/obsidian express
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or with yarn:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
yarn add @periodic/obsidian express
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Peer Dependencies:**
|
|
64
|
+
- `express` ^4.0.0 || ^5.0.0
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## ๐ Quick Start
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import express from 'express';
|
|
72
|
+
import { obsidian, errorHandler } from '@periodic/obsidian';
|
|
73
|
+
|
|
74
|
+
const app = express();
|
|
75
|
+
|
|
76
|
+
// Throw errors anywhere in your routes
|
|
77
|
+
app.get('/users/:id', (req, res) => {
|
|
78
|
+
const user = findUser(req.params.id);
|
|
79
|
+
|
|
80
|
+
if (!user) {
|
|
81
|
+
throw obsidian.notFound('User not found', {
|
|
82
|
+
code: 'USER_NOT_FOUND',
|
|
83
|
+
details: { userId: req.params.id }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
res.json(user);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Handle all errors automatically
|
|
91
|
+
app.use(errorHandler());
|
|
92
|
+
|
|
93
|
+
app.listen(3000);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Error Response:**
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"status": 404,
|
|
100
|
+
"message": "User not found",
|
|
101
|
+
"code": "USER_NOT_FOUND",
|
|
102
|
+
"details": {
|
|
103
|
+
"userId": "123"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## ๐ง Core Concepts
|
|
111
|
+
|
|
112
|
+
### The `obsidian` Object
|
|
113
|
+
|
|
114
|
+
- **`obsidian` is a factory namespace**
|
|
115
|
+
- It exposes one method per HTTP status code
|
|
116
|
+
- Each method returns an instance of `HttpError`
|
|
117
|
+
- **This is the primary API intended for application code**
|
|
118
|
+
- Covers all standard HTTP status codes (100โ511)
|
|
119
|
+
|
|
120
|
+
**Typical usage:**
|
|
121
|
+
- Application code throws errors using `obsidian.*()`
|
|
122
|
+
- Keeps error creation consistent and readable
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
throw obsidian.notFound('User not found');
|
|
126
|
+
throw obsidian.badRequest('Invalid input');
|
|
127
|
+
throw obsidian.unauthorized('Authentication required');
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### The `HttpError` Class
|
|
131
|
+
|
|
132
|
+
- **`HttpError` is the single foundational error class in the library**
|
|
133
|
+
- All `obsidian.*()` methods internally create `HttpError` instances
|
|
134
|
+
- **Intended for:**
|
|
135
|
+
- `instanceof HttpError` checks
|
|
136
|
+
- Framework adapters and middleware
|
|
137
|
+
- Advanced or non-standard error handling
|
|
138
|
+
|
|
139
|
+
**Design principle:**
|
|
140
|
+
> Users throw errors using `obsidian`, frameworks handle errors using `HttpError`.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// Throwing (application code)
|
|
144
|
+
throw obsidian.notFound('User not found');
|
|
145
|
+
|
|
146
|
+
// Handling (middleware/framework code)
|
|
147
|
+
if (error instanceof HttpError) {
|
|
148
|
+
res.status(error.status).json(error.toJSON());
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## โจ Features
|
|
155
|
+
|
|
156
|
+
### ๐ท๏ธ Complete Status Code Coverage
|
|
157
|
+
|
|
158
|
+
Every standard HTTP status code from 100 to 511:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// 1xx Informational
|
|
162
|
+
obsidian.continue()
|
|
163
|
+
obsidian.processing()
|
|
164
|
+
|
|
165
|
+
// 2xx Success
|
|
166
|
+
obsidian.ok()
|
|
167
|
+
obsidian.created()
|
|
168
|
+
|
|
169
|
+
// 3xx Redirection
|
|
170
|
+
obsidian.movedPermanently()
|
|
171
|
+
obsidian.temporaryRedirect()
|
|
172
|
+
|
|
173
|
+
// 4xx Client Errors
|
|
174
|
+
obsidian.badRequest()
|
|
175
|
+
obsidian.unauthorized()
|
|
176
|
+
obsidian.forbidden()
|
|
177
|
+
obsidian.notFound()
|
|
178
|
+
obsidian.unprocessableEntity()
|
|
179
|
+
|
|
180
|
+
// 5xx Server Errors
|
|
181
|
+
obsidian.internalServerError()
|
|
182
|
+
obsidian.serviceUnavailable()
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### ๐ฏ Rich Error Metadata
|
|
186
|
+
|
|
187
|
+
Add structured context to your errors:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
throw obsidian.unprocessableEntity('Validation failed', {
|
|
191
|
+
code: 'VALIDATION_ERROR',
|
|
192
|
+
details: {
|
|
193
|
+
errors: [
|
|
194
|
+
{ field: 'email', message: 'Invalid email format' },
|
|
195
|
+
{ field: 'age', message: 'Must be 18 or older' }
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### ๐ก๏ธ Production-Ready Middleware
|
|
202
|
+
|
|
203
|
+
Built-in Express middleware with configurable options:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
app.use(errorHandler({
|
|
207
|
+
// Include stack traces in development
|
|
208
|
+
includeStack: process.env.NODE_ENV !== 'production',
|
|
209
|
+
|
|
210
|
+
// Custom error logging
|
|
211
|
+
logger: (error, req) => {
|
|
212
|
+
console.error({
|
|
213
|
+
error: error.message,
|
|
214
|
+
path: req.path,
|
|
215
|
+
method: req.method,
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// Transform error responses
|
|
220
|
+
transform: (error) => ({
|
|
221
|
+
...error.toJSON(),
|
|
222
|
+
timestamp: new Date().toISOString(),
|
|
223
|
+
}),
|
|
224
|
+
}));
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## ๐ Common Patterns
|
|
230
|
+
|
|
231
|
+
### 1. Authentication Errors
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { obsidian } from '@periodic/obsidian';
|
|
235
|
+
|
|
236
|
+
function requireAuth(req, res, next) {
|
|
237
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
238
|
+
|
|
239
|
+
if (!token) {
|
|
240
|
+
throw obsidian.unauthorized('Authentication required', {
|
|
241
|
+
code: 'NO_TOKEN'
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
req.user = verifyToken(token);
|
|
247
|
+
next();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw obsidian.unauthorized('Invalid or expired token', {
|
|
250
|
+
code: 'INVALID_TOKEN'
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 2. Permission Checks
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
function requireRole(role: string) {
|
|
260
|
+
return (req, res, next) => {
|
|
261
|
+
if (!req.user) {
|
|
262
|
+
throw obsidian.unauthorized('Authentication required');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (req.user.role !== role) {
|
|
266
|
+
throw obsidian.forbidden('Insufficient permissions', {
|
|
267
|
+
code: 'INSUFFICIENT_PERMISSIONS',
|
|
268
|
+
details: {
|
|
269
|
+
required: role,
|
|
270
|
+
current: req.user.role
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
next();
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
app.delete('/users/:id', requireRole('admin'), deleteUserHandler);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 3. Validation Errors
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
function validateUser(data: any) {
|
|
286
|
+
const errors = [];
|
|
287
|
+
|
|
288
|
+
if (!data.email || !isValidEmail(data.email)) {
|
|
289
|
+
errors.push({ field: 'email', message: 'Invalid email format' });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!data.age || data.age < 18) {
|
|
293
|
+
errors.push({ field: 'age', message: 'Must be 18 or older' });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (errors.length > 0) {
|
|
297
|
+
throw obsidian.unprocessableEntity('Validation failed', {
|
|
298
|
+
code: 'VALIDATION_ERROR',
|
|
299
|
+
details: { errors }
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### 4. Rate Limiting Integration
|
|
306
|
+
|
|
307
|
+
Works seamlessly with **@periodic/titanium**:
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
import { rateLimit } from '@periodic/titanium';
|
|
311
|
+
import { obsidian } from '@periodic/obsidian';
|
|
312
|
+
|
|
313
|
+
app.use(rateLimit({
|
|
314
|
+
redis,
|
|
315
|
+
limit: 100,
|
|
316
|
+
window: 60,
|
|
317
|
+
keyPrefix: 'api',
|
|
318
|
+
// Custom error handling
|
|
319
|
+
onLimitExceeded: (req) => {
|
|
320
|
+
throw obsidian.tooManyRequests('Rate limit exceeded', {
|
|
321
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
322
|
+
details: { retryAfter: 60 }
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}));
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 5. Resource Conflicts
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
app.post('/users', async (req, res, next) => {
|
|
332
|
+
try {
|
|
333
|
+
const existing = await findUserByEmail(req.body.email);
|
|
334
|
+
|
|
335
|
+
if (existing) {
|
|
336
|
+
throw obsidian.conflict('Email already registered', {
|
|
337
|
+
code: 'EMAIL_CONFLICT',
|
|
338
|
+
details: { email: req.body.email }
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const user = await createUser(req.body);
|
|
343
|
+
res.status(201).json(user);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
next(error);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 6. Domain-Specific Error Helpers
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
export const UserErrors = {
|
|
354
|
+
notFound: (userId: string) =>
|
|
355
|
+
obsidian.notFound('User not found', {
|
|
356
|
+
code: 'USER_NOT_FOUND',
|
|
357
|
+
details: { userId }
|
|
358
|
+
}),
|
|
359
|
+
|
|
360
|
+
emailConflict: (email: string) =>
|
|
361
|
+
obsidian.conflict('Email already registered', {
|
|
362
|
+
code: 'EMAIL_CONFLICT',
|
|
363
|
+
details: { email }
|
|
364
|
+
}),
|
|
365
|
+
|
|
366
|
+
invalidPassword: () =>
|
|
367
|
+
obsidian.badRequest('Password must be at least 8 characters', {
|
|
368
|
+
code: 'INVALID_PASSWORD'
|
|
369
|
+
}),
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Usage
|
|
373
|
+
throw UserErrors.notFound('123');
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## ๐๏ธ Configuration Options
|
|
379
|
+
|
|
380
|
+
### Error Handler Middleware
|
|
381
|
+
|
|
382
|
+
| Option | Type | Default | Description |
|
|
383
|
+
|--------|------|---------|-------------|
|
|
384
|
+
| `includeStack` | `boolean` | `false` (prod) | Include stack traces in responses |
|
|
385
|
+
| `logger` | `(error, req) => void` | - | Custom error logging function |
|
|
386
|
+
| `transform` | `(error) => object` | - | Transform error JSON response |
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { errorHandler } from '@periodic/obsidian';
|
|
390
|
+
|
|
391
|
+
app.use(errorHandler({
|
|
392
|
+
includeStack: process.env.NODE_ENV !== 'production',
|
|
393
|
+
logger: (error, req) => {
|
|
394
|
+
// Your logging logic (e.g., Sentry, DataDog)
|
|
395
|
+
},
|
|
396
|
+
transform: (error) => ({
|
|
397
|
+
...error.toJSON(),
|
|
398
|
+
timestamp: new Date().toISOString(),
|
|
399
|
+
requestId: req.id,
|
|
400
|
+
}),
|
|
401
|
+
}));
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Simple Error Handler
|
|
405
|
+
|
|
406
|
+
For minimal setup:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { simpleErrorHandler } from '@periodic/obsidian';
|
|
410
|
+
|
|
411
|
+
// Only handles HttpError instances, passes others to next handler
|
|
412
|
+
app.use(simpleErrorHandler());
|
|
413
|
+
|
|
414
|
+
// Add your own fallback
|
|
415
|
+
app.use((err, req, res, next) => {
|
|
416
|
+
res.status(500).json({ error: 'Something went wrong' });
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## ๐ Status Code Reference
|
|
423
|
+
|
|
424
|
+
Obsidian provides factory helpers for every standard HTTP status code (100โ511).
|
|
425
|
+
|
|
426
|
+
๐ **See the complete mapping here:** [STATUS_CODES.md](STATUS_CODES.md)
|
|
427
|
+
|
|
428
|
+
**Quick Reference:**
|
|
429
|
+
|
|
430
|
+
<details>
|
|
431
|
+
<summary><strong>1xx Informational (4 codes)</strong></summary>
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
obsidian.continue() // 100
|
|
435
|
+
obsidian.switchingProtocols() // 101
|
|
436
|
+
obsidian.processing() // 102
|
|
437
|
+
obsidian.earlyHints() // 103
|
|
438
|
+
```
|
|
439
|
+
</details>
|
|
440
|
+
|
|
441
|
+
<details>
|
|
442
|
+
<summary><strong>2xx Success (10 codes)</strong></summary>
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
obsidian.ok() // 200
|
|
446
|
+
obsidian.created() // 201
|
|
447
|
+
obsidian.accepted() // 202
|
|
448
|
+
obsidian.noContent() // 204
|
|
449
|
+
// ... and 6 more
|
|
450
|
+
```
|
|
451
|
+
</details>
|
|
452
|
+
|
|
453
|
+
<details>
|
|
454
|
+
<summary><strong>3xx Redirection (8 codes)</strong></summary>
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
obsidian.movedPermanently() // 301
|
|
458
|
+
obsidian.found() // 302
|
|
459
|
+
obsidian.notModified() // 304
|
|
460
|
+
obsidian.temporaryRedirect() // 307
|
|
461
|
+
// ... and 4 more
|
|
462
|
+
```
|
|
463
|
+
</details>
|
|
464
|
+
|
|
465
|
+
<details>
|
|
466
|
+
<summary><strong>4xx Client Errors โ All standard codes</strong></summary>
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
obsidian.badRequest() // 400
|
|
470
|
+
obsidian.unauthorized() // 401
|
|
471
|
+
obsidian.forbidden() // 403
|
|
472
|
+
obsidian.notFound() // 404
|
|
473
|
+
obsidian.conflict() // 409
|
|
474
|
+
obsidian.unprocessableEntity() // 422
|
|
475
|
+
obsidian.tooManyRequests() // 429
|
|
476
|
+
// ... and 22 more
|
|
477
|
+
```
|
|
478
|
+
</details>
|
|
479
|
+
|
|
480
|
+
<details>
|
|
481
|
+
<summary><strong>5xx Server Errors โ All standard codes</strong></summary>
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
obsidian.internalServerError() // 500
|
|
485
|
+
obsidian.notImplemented() // 501
|
|
486
|
+
obsidian.badGateway() // 502
|
|
487
|
+
obsidian.serviceUnavailable() // 503
|
|
488
|
+
obsidian.gatewayTimeout() // 504
|
|
489
|
+
// ... and 6 more
|
|
490
|
+
```
|
|
491
|
+
</details>
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## ๐ง API Reference
|
|
496
|
+
|
|
497
|
+
### `obsidian` Object
|
|
498
|
+
|
|
499
|
+
Main namespace with all error factory methods:
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { obsidian } from '@periodic/obsidian';
|
|
503
|
+
|
|
504
|
+
obsidian.notFound(message?: string, options?: HttpErrorOptions)
|
|
505
|
+
obsidian.badRequest(message?: string, options?: HttpErrorOptions)
|
|
506
|
+
obsidian.unauthorized(message?: string, options?: HttpErrorOptions)
|
|
507
|
+
// ... all standard HTTP status codes
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Parameters:**
|
|
511
|
+
- `message` - Custom error message (optional, uses default if omitted)
|
|
512
|
+
- `options.code` - Machine-readable error code
|
|
513
|
+
- `options.details` - Additional error context
|
|
514
|
+
|
|
515
|
+
**Returns:** `HttpError` instance
|
|
516
|
+
|
|
517
|
+
### `HttpError` Class
|
|
518
|
+
|
|
519
|
+
Base error class:
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { HttpError } from '@periodic/obsidian';
|
|
523
|
+
|
|
524
|
+
const error = new HttpError(404, 'Not found', {
|
|
525
|
+
code: 'RESOURCE_NOT_FOUND',
|
|
526
|
+
details: { resourceId: '123' }
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Properties
|
|
530
|
+
error.status // 404
|
|
531
|
+
error.message // 'Not found'
|
|
532
|
+
error.code // 'RESOURCE_NOT_FOUND'
|
|
533
|
+
error.details // { resourceId: '123' }
|
|
534
|
+
|
|
535
|
+
// Methods
|
|
536
|
+
error.toJSON() // Serialize without stack trace
|
|
537
|
+
HttpError.getDefaultMessage(404) // 'Not Found'
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Middleware Functions
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
import { errorHandler, simpleErrorHandler } from '@periodic/obsidian';
|
|
544
|
+
|
|
545
|
+
// Full-featured handler
|
|
546
|
+
errorHandler(options?: ExpressErrorHandlerOptions)
|
|
547
|
+
|
|
548
|
+
// Minimal handler
|
|
549
|
+
simpleErrorHandler()
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## ๐ Framework Integration
|
|
555
|
+
|
|
556
|
+
### Express.js (Built-in)
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
import express from 'express';
|
|
560
|
+
import { obsidian, errorHandler } from '@periodic/obsidian';
|
|
561
|
+
|
|
562
|
+
const app = express();
|
|
563
|
+
|
|
564
|
+
app.get('/users/:id', (req, res) => {
|
|
565
|
+
throw obsidian.notFound('User not found');
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
app.use(errorHandler());
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
> **Note:** Obsidian is framework-agnostic. No official Fastify or NestJS adapters are provided yet; the examples below demonstrate manual integration.
|
|
572
|
+
|
|
573
|
+
### Fastify
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
import Fastify from 'fastify';
|
|
577
|
+
import { HttpError, obsidian } from '@periodic/obsidian';
|
|
578
|
+
|
|
579
|
+
const fastify = Fastify();
|
|
580
|
+
|
|
581
|
+
fastify.get('/users/:id', async (request, reply) => {
|
|
582
|
+
throw obsidian.notFound('User not found');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
fastify.setErrorHandler((error, request, reply) => {
|
|
586
|
+
if (error instanceof HttpError) {
|
|
587
|
+
return reply.status(error.status).send(error.toJSON());
|
|
588
|
+
}
|
|
589
|
+
reply.status(500).send({ error: 'Internal Server Error' });
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### NestJS
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
import { Controller, Get, Param } from '@nestjs/common';
|
|
597
|
+
import { obsidian } from '@periodic/obsidian';
|
|
598
|
+
|
|
599
|
+
@Controller('users')
|
|
600
|
+
export class UsersController {
|
|
601
|
+
@Get(':id')
|
|
602
|
+
async getUser(@Param('id') id: string) {
|
|
603
|
+
const user = await this.userService.findById(id);
|
|
604
|
+
|
|
605
|
+
if (!user) {
|
|
606
|
+
throw obsidian.notFound('User not found', {
|
|
607
|
+
code: 'USER_NOT_FOUND'
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return user;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## ๐ ๏ธ Production Recommendations
|
|
619
|
+
|
|
620
|
+
### Error Response Structure
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
// Development (with includeStack: true)
|
|
624
|
+
{
|
|
625
|
+
"status": 500,
|
|
626
|
+
"message": "Database connection failed",
|
|
627
|
+
"code": "DB_CONNECTION_ERROR",
|
|
628
|
+
"details": { ... },
|
|
629
|
+
"stack": "Error: Database connection failed\n at ..."
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Production (with includeStack: false)
|
|
633
|
+
{
|
|
634
|
+
"status": 500,
|
|
635
|
+
"message": "Internal Server Error"
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Logging Best Practices
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
import winston from 'winston';
|
|
643
|
+
|
|
644
|
+
const logger = winston.createLogger({
|
|
645
|
+
level: 'info',
|
|
646
|
+
format: winston.format.json(),
|
|
647
|
+
transports: [new winston.transports.Console()]
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
app.use(errorHandler({
|
|
651
|
+
logger: (error, req) => {
|
|
652
|
+
if (error instanceof HttpError && error.status >= 500) {
|
|
653
|
+
// Log server errors with full context
|
|
654
|
+
logger.error({
|
|
655
|
+
message: error.message,
|
|
656
|
+
code: error.code,
|
|
657
|
+
path: req.path,
|
|
658
|
+
method: req.method,
|
|
659
|
+
stack: error.stack,
|
|
660
|
+
});
|
|
661
|
+
} else if (error instanceof HttpError) {
|
|
662
|
+
// Log client errors without stack
|
|
663
|
+
logger.warn({
|
|
664
|
+
message: error.message,
|
|
665
|
+
code: error.code,
|
|
666
|
+
path: req.path,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
}));
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Environment-Specific Configuration
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
677
|
+
|
|
678
|
+
app.use(errorHandler({
|
|
679
|
+
includeStack: isDevelopment,
|
|
680
|
+
logger: isDevelopment
|
|
681
|
+
? (error) => console.error(error)
|
|
682
|
+
: (error) => logToSentry(error),
|
|
683
|
+
}));
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### Recommended Error Codes
|
|
687
|
+
|
|
688
|
+
Use consistent, descriptive error codes:
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
// Authentication & Authorization
|
|
692
|
+
'AUTH_TOKEN_MISSING'
|
|
693
|
+
'AUTH_TOKEN_INVALID'
|
|
694
|
+
'AUTH_TOKEN_EXPIRED'
|
|
695
|
+
'PERMISSION_DENIED'
|
|
696
|
+
|
|
697
|
+
// Validation
|
|
698
|
+
'VALIDATION_ERROR'
|
|
699
|
+
'INVALID_EMAIL'
|
|
700
|
+
'INVALID_PASSWORD'
|
|
701
|
+
|
|
702
|
+
// Resources
|
|
703
|
+
'USER_NOT_FOUND'
|
|
704
|
+
'RESOURCE_NOT_FOUND'
|
|
705
|
+
'EMAIL_CONFLICT'
|
|
706
|
+
|
|
707
|
+
// Business Logic
|
|
708
|
+
'INSUFFICIENT_BALANCE'
|
|
709
|
+
'ORDER_ALREADY_PROCESSED'
|
|
710
|
+
'SUBSCRIPTION_EXPIRED'
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## ๐จ TypeScript Support
|
|
716
|
+
|
|
717
|
+
Full TypeScript support with complete type safety:
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
import type {
|
|
721
|
+
HttpError,
|
|
722
|
+
HttpErrorOptions,
|
|
723
|
+
HttpErrorJSON
|
|
724
|
+
} from '@periodic/obsidian';
|
|
725
|
+
|
|
726
|
+
function handleError(error: unknown) {
|
|
727
|
+
if (error instanceof HttpError) {
|
|
728
|
+
console.log(error.status); // number
|
|
729
|
+
console.log(error.message); // string
|
|
730
|
+
console.log(error.code); // string | undefined
|
|
731
|
+
console.log(error.details); // unknown
|
|
732
|
+
|
|
733
|
+
const json: HttpErrorJSON = error.toJSON();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
## ๐งฉ Architecture
|
|
741
|
+
|
|
742
|
+
```
|
|
743
|
+
@periodic/obsidian/
|
|
744
|
+
โโโ src/
|
|
745
|
+
โ โโโ core/ # Framework-agnostic
|
|
746
|
+
โ โ โโโ types.ts # TypeScript interfaces
|
|
747
|
+
โ โ โโโ status-codes.ts # HTTP status codes
|
|
748
|
+
โ โ โโโ http-error.ts # Base error class
|
|
749
|
+
โ โ โโโ factories.ts # Error factories
|
|
750
|
+
โ โโโ adapters/ # Framework integration
|
|
751
|
+
โ โ โโโ express.ts # Express middleware
|
|
752
|
+
โ โโโ index.ts # Public API
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
**Design Philosophy:**
|
|
756
|
+
- **Core** is pure TypeScript with no framework dependencies
|
|
757
|
+
- **Adapters** connect core to specific frameworks
|
|
758
|
+
- Easy to extend for other frameworks (Koa, Hapi, etc.)
|
|
759
|
+
- Can be used in non-Express applications via the core module
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## ๐ Performance
|
|
764
|
+
|
|
765
|
+
Obsidian is designed for minimal overhead:
|
|
766
|
+
|
|
767
|
+
- **Zero runtime dependencies** (except Express peer dependency)
|
|
768
|
+
- **Lazy initialization** of error objects
|
|
769
|
+
- **Efficient serialization** without unnecessary cloning
|
|
770
|
+
- **No I/O operations** in error creation path
|
|
771
|
+
- **Lightweight** - less than 10KB gzipped
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## ๐ซ Explicit Non-Goals
|
|
776
|
+
|
|
777
|
+
This package **intentionally does not** include:
|
|
778
|
+
|
|
779
|
+
โ Error tracking/monitoring (use Sentry, Datadog, etc.)
|
|
780
|
+
โ Internationalization (handle in your application layer)
|
|
781
|
+
โ Request validation (use Joi, Yup, Zod, etc.)
|
|
782
|
+
โ Automatic retry logic
|
|
783
|
+
โ Circuit breakers
|
|
784
|
+
โ In-built logging (provide your own logger)
|
|
785
|
+
|
|
786
|
+
Focus on doing one thing well: **structured HTTP error handling**.
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## ๐ค Related Packages
|
|
791
|
+
|
|
792
|
+
Part of the **Periodic** series by Uday Thakur:
|
|
793
|
+
|
|
794
|
+
- [**@periodic/titanium**](https://www.npmjs.com/package/@periodic/titanium) - Redis-backed rate limiting middleware
|
|
795
|
+
- [**@periodic/osmium**](https://www.npmjs.com/package/@periodic/osmium) - Redis caching middleware for Express
|
|
796
|
+
|
|
797
|
+
Build complete, production-ready APIs with the Periodic series!
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
## ๐ Documentation
|
|
802
|
+
|
|
803
|
+
- [Quick Start Guide](QUICKSTART.md)
|
|
804
|
+
- [Status Code Reference](STATUS_CODES.md)
|
|
805
|
+
- [Contributing Guide](CONTRIBUTING.md)
|
|
806
|
+
- [Changelog](CHANGELOG.md)
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
## ๐งช Testing
|
|
811
|
+
|
|
812
|
+
```bash
|
|
813
|
+
# Run tests
|
|
814
|
+
npm test
|
|
815
|
+
|
|
816
|
+
# Run tests with coverage
|
|
817
|
+
npm run test:coverage
|
|
818
|
+
|
|
819
|
+
# Run tests in watch mode
|
|
820
|
+
npm run test:watch
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
**Note:** All tests are comprehensive and achieve >95% code coverage.
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## ๐ License
|
|
828
|
+
|
|
829
|
+
MIT ยฉ [Uday Thakur](LICENSE)
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## ๐ Contributing
|
|
834
|
+
|
|
835
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on:
|
|
836
|
+
|
|
837
|
+
- Code of conduct
|
|
838
|
+
- Development setup
|
|
839
|
+
- Pull request process
|
|
840
|
+
- Coding standards
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## ๐ Support
|
|
845
|
+
|
|
846
|
+
- ๐ง **Email:** udaythakurwork@gmail.com
|
|
847
|
+
- ๐ **Issues:** [GitHub Issues](https://github.com/yourusername/periodic-obsidian/issues)
|
|
848
|
+
- ๐ฌ **Discussions:** [GitHub Discussions](https://github.com/yourusername/periodic-obsidian/discussions)
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
## ๐ Show Your Support
|
|
853
|
+
|
|
854
|
+
Give a โญ๏ธ if this project helped you build better APIs!
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
**Built with โค๏ธ by Uday Thakur for production-grade Node.js applications**
|