@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/README.md ADDED
@@ -0,0 +1,858 @@
1
+ # โšซ Periodic Obsidian
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@periodic/obsidian.svg)](https://www.npmjs.com/package/@periodic/obsidian)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](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**