node-responder 1.0.0 → 1.2.1

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 CHANGED
@@ -3,11 +3,13 @@
3
3
  > Modern, standardized API response middleware for Express.js
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/node-responder.svg)](https://www.npmjs.com/package/node-responder)
6
+ [![npm downloads](https://img.shields.io/npm/dm/node-responder.svg)](https://www.npmjs.com/package/node-responder)
6
7
  [![license](https://img.shields.io/npm/l/node-responder.svg)](LICENSE)
7
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-supported-blue.svg)](types/index.d.ts)
8
9
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-green.svg)]()
9
10
 
10
- Stop writing repetitive `res.status(200).json({ success: true, data: ... })` in every route. `node-responder` adds clean, consistent response helpers directly to Express's `res` object.
11
+ Stop writing repetitive `res.status(200).json({ success: true, data: ... })` in every route.
12
+ `node-responder` adds clean, consistent response helpers directly to Express's `res` object.
11
13
 
12
14
  ---
13
15
 
@@ -16,9 +18,10 @@ Stop writing repetitive `res.status(200).json({ success: true, data: ... })` in
16
18
  - ✅ **Zero dependencies** — only Express as a peer dependency
17
19
  - ✅ **TypeScript support** — full type definitions included
18
20
  - ✅ **ESM + CommonJS** — works with both `require` and `import`
21
+ - ✅ **Request Logger** — logs method, URL, status, and response time to terminal
19
22
  - ✅ **Pagination built-in** — `res.paginate()` with full meta
23
+ - ✅ **asyncHandler** — write async routes without try-catch boilerplate
20
24
  - ✅ **Consistent format** — every response follows the same structure
21
- - ✅ **Shorthand methods** — `res.ok()`, `res.notFound()`, `res.unauthorized()` and more
22
25
  - ✅ **Node.js 14+** supported
23
26
 
24
27
  ---
@@ -38,15 +41,14 @@ const express = require("express");
38
41
  const responder = require("node-responder");
39
42
 
40
43
  const app = express();
44
+ app.use(express.json());
41
45
 
42
46
  // Apply middleware globally
43
47
  app.use(responder());
44
48
 
45
49
  app.get("/user/:id", async (req, res) => {
46
50
  const user = await User.findById(req.params.id);
47
-
48
51
  if (!user) return res.notFound("User not found");
49
-
50
52
  return res.ok(user);
51
53
  });
52
54
 
@@ -57,23 +59,23 @@ app.listen(3000);
57
59
 
58
60
  ## 📋 Response Format
59
61
 
60
- All responses follow this consistent structure:
62
+ Every response follows the same consistent structure:
61
63
 
62
- **Success:**
64
+ ### Success Response
63
65
 
64
66
  ```json
65
67
  {
66
68
  "success": true,
67
69
  "message": "Success",
68
- "data": { ... },
70
+ "data": { "id": 1, "name": "John" },
69
71
  "meta": {
70
- "timestamp": "2024-01-15T10:30:00.000Z",
72
+ "timestamp": "2025-01-15T10:30:00.000Z",
71
73
  "statusCode": 200
72
74
  }
73
75
  }
74
76
  ```
75
77
 
76
- **Error:**
78
+ ### Error Response
77
79
 
78
80
  ```json
79
81
  {
@@ -82,21 +84,21 @@ All responses follow this consistent structure:
82
84
  "data": null,
83
85
  "errors": null,
84
86
  "meta": {
85
- "timestamp": "2024-01-15T10:30:00.000Z",
87
+ "timestamp": "2025-01-15T10:30:00.000Z",
86
88
  "statusCode": 404
87
89
  }
88
90
  }
89
91
  ```
90
92
 
91
- **Paginated:**
93
+ ### Paginated Response
92
94
 
93
95
  ```json
94
96
  {
95
97
  "success": true,
96
98
  "message": "Users fetched",
97
- "data": [ ... ],
99
+ "data": [{ "id": 1 }, { "id": 2 }],
98
100
  "meta": {
99
- "timestamp": "2024-01-15T10:30:00.000Z",
101
+ "timestamp": "2025-01-15T10:30:00.000Z",
100
102
  "statusCode": 200,
101
103
  "pagination": {
102
104
  "page": 1,
@@ -112,121 +114,468 @@ All responses follow this consistent structure:
112
114
 
113
115
  ---
114
116
 
115
- ## 📖 API Reference
116
-
117
- ### Middleware Setup
117
+ ## ⚙️ Middleware Setup
118
118
 
119
119
  ```js
120
120
  const responder = require("node-responder");
121
121
 
122
- app.use(responder()); // attach to all routes
123
- // or
124
- router.use(responder()); // attach to specific router
122
+ // Apply to all routes
123
+ app.use(responder());
124
+
125
+ // With logger enabled
126
+ app.use(responder({ logger: true }));
127
+
128
+ // Apply to a specific router only
129
+ router.use(responder());
125
130
  ```
126
131
 
132
+ ### Options
133
+
134
+ | Option | Type | Default | Description |
135
+ | -------- | --------- | ------- | ------------------------------------------------------------------------- |
136
+ | `logger` | `boolean` | `false` | Logs each request to terminal with method, URL, status, and response time |
137
+
138
+ ---
139
+
140
+ ## 📖 API Reference
141
+
142
+ ### Quick Reference
143
+
144
+ | Method | Status | When to use |
145
+ | -------------------------------------------- | ------ | ------------------------------------------ |
146
+ | `res.ok(data?, message?)` | 200 | Successful GET request |
147
+ | `res.created(data?, message?)` | 201 | New resource created |
148
+ | `res.noContent()` | 204 | Delete or update with no data to return |
149
+ | `res.success(data?, message?, statusCode?)` | custom | Custom success status code |
150
+ | `res.badRequest(message?, errors?)` | 400 | Validation failed |
151
+ | `res.unauthorized(message?)` | 401 | Not logged in or no token |
152
+ | `res.forbidden(message?)` | 403 | Logged in but no permission |
153
+ | `res.notFound(message?)` | 404 | Resource does not exist |
154
+ | `res.conflict(message?)` | 409 | Duplicate data (e.g. email already exists) |
155
+ | `res.unprocessable(message?, errors?)` | 422 | Business logic validation failed |
156
+ | `res.tooManyRequests(message?, retryAfter?)` | 429 | Rate limit exceeded |
157
+ | `res.serverError(message?)` | 500 | Unexpected server-side error |
158
+ | `res.error(message?, statusCode?, errors?)` | custom | Custom error status code |
159
+ | `res.paginate(data, message?, pagination?)` | 200 | Paginated list response |
160
+
127
161
  ---
128
162
 
129
163
  ### ✅ Success Methods
130
164
 
131
- | Method | Status | Description |
132
- | ---------------------------------------- | ------ | ---------------- |
133
- | `res.success(data, message, statusCode)` | custom | Generic success |
134
- | `res.ok(data, message)` | 200 | Standard OK |
135
- | `res.created(data, message)` | 201 | Resource created |
136
- | `res.noContent()` | 204 | No content |
165
+ ---
166
+
167
+ #### `res.ok(data?, message?)`
168
+
169
+ **Status: 200** Use for standard successful GET requests.
137
170
 
138
171
  ```js
139
- res.ok({ id: 1, name: "Rahim" });
140
- res.created({ id: 5 }, "User created successfully");
141
- res.success(data, "Custom message", 200);
172
+ // With data only
173
+ res.ok({ id: 1, name: "John" });
174
+
175
+ // With data and custom message
176
+ res.ok({ id: 1, name: "John" }, "User fetched successfully");
177
+
178
+ // Response:
179
+ // { success: true, message: "User fetched successfully", data: { id: 1, name: "John" }, meta: { statusCode: 200, ... } }
180
+ ```
181
+
182
+ ---
183
+
184
+ #### `res.created(data?, message?)`
185
+
186
+ **Status: 201** — Use when a new resource has been successfully created.
187
+
188
+ ```js
189
+ const user = await User.create({ name: "John", email: "john@example.com" });
190
+
191
+ res.created(user);
192
+
193
+ // With custom message
194
+ res.created(user, "Account created successfully");
195
+
196
+ // Response:
197
+ // { success: true, message: "Created successfully", data: { ...user }, meta: { statusCode: 201, ... } }
198
+ ```
199
+
200
+ ---
201
+
202
+ #### `res.noContent()`
203
+
204
+ **Status: 204** — Use after a successful delete or update when no data needs to be returned.
205
+
206
+ ```js
207
+ await User.findByIdAndDelete(req.params.id);
208
+ res.noContent();
209
+
210
+ // Response: empty body (HTTP 204 No Content)
211
+ ```
212
+
213
+ ---
214
+
215
+ #### `res.success(data?, message?, statusCode?)`
216
+
217
+ **Status: custom** — Use when you need a custom success status code.
218
+
219
+ ```js
220
+ res.success({ accepted: true }, "Request accepted", 202);
221
+
222
+ // Response:
223
+ // { success: true, message: "Request accepted", data: { accepted: true }, meta: { statusCode: 202, ... } }
142
224
  ```
143
225
 
144
226
  ---
145
227
 
146
228
  ### ❌ Error Methods
147
229
 
148
- | Method | Status | Description |
149
- | ---------------------------------------- | ------ | -------------------- |
150
- | `res.error(message, statusCode, errors)` | custom | Generic error |
151
- | `res.badRequest(message, errors)` | 400 | Validation failed |
152
- | `res.unauthorized(message)` | 401 | Not authenticated |
153
- | `res.forbidden(message)` | 403 | Not authorized |
154
- | `res.notFound(message)` | 404 | Resource not found |
155
- | `res.conflict(message)` | 409 | Conflict |
156
- | `res.unprocessable(message, errors)` | 422 | Unprocessable entity |
157
- | `res.serverError(message)` | 500 | Server error |
230
+ ---
231
+
232
+ #### `res.badRequest(message?, errors?)`
233
+
234
+ **Status: 400** Use when the request is malformed or validation fails.
235
+
236
+ ```js
237
+ // Simple message
238
+ res.badRequest("Name is required");
239
+
240
+ // With validation errors object
241
+ res.badRequest("Validation failed", {
242
+ name: "Name is required",
243
+ email: "Invalid email format",
244
+ });
245
+
246
+ // Response:
247
+ // { success: false, message: "Validation failed", data: null, errors: { name: "...", email: "..." }, meta: { statusCode: 400, ... } }
248
+ ```
249
+
250
+ ---
251
+
252
+ #### `res.unauthorized(message?)`
253
+
254
+ **Status: 401** — Use when the user is not authenticated (no token or invalid token).
255
+
256
+ ```js
257
+ // Default message
258
+ res.unauthorized();
259
+
260
+ // Custom message
261
+ res.unauthorized("Please login to continue");
262
+
263
+ // Response:
264
+ // { success: false, message: "Unauthorized", data: null, errors: null, meta: { statusCode: 401, ... } }
265
+ ```
266
+
267
+ ---
268
+
269
+ #### `res.forbidden(message?)`
270
+
271
+ **Status: 403** — Use when the user is authenticated but does not have permission.
158
272
 
159
273
  ```js
160
- res.notFound("Product not found");
161
- res.unauthorized("Please login first");
162
- res.badRequest("Validation failed", { email: "Email is required" });
274
+ // Default message
275
+ res.forbidden();
276
+
277
+ // Custom message
278
+ res.forbidden("You do not have permission to access this resource");
279
+
280
+ // Response:
281
+ // { success: false, message: "Forbidden", data: null, errors: null, meta: { statusCode: 403, ... } }
282
+ ```
283
+
284
+ ---
285
+
286
+ #### `res.notFound(message?)`
287
+
288
+ **Status: 404** — Use when a requested resource does not exist.
289
+
290
+ ```js
291
+ const user = await User.findById(req.params.id);
292
+
293
+ if (!user) {
294
+ return res.notFound("User not found");
295
+ }
296
+
297
+ // Response:
298
+ // { success: false, message: "User not found", data: null, errors: null, meta: { statusCode: 404, ... } }
163
299
  ```
164
300
 
165
301
  ---
166
302
 
167
- ### 📄 Pagination
303
+ #### `res.conflict(message?)`
304
+
305
+ **Status: 409** — Use when the request conflicts with existing data (e.g. duplicate email).
168
306
 
169
307
  ```js
170
- const users = await User.find().skip(skip).limit(limit);
171
- const total = await User.countDocuments();
308
+ const exists = await User.findOne({ email: req.body.email });
309
+
310
+ if (exists) {
311
+ return res.conflict("Email already registered");
312
+ }
313
+
314
+ // Response:
315
+ // { success: false, message: "Email already registered", data: null, errors: null, meta: { statusCode: 409, ... } }
316
+ ```
317
+
318
+ ---
319
+
320
+ #### `res.unprocessable(message?, errors?)`
321
+
322
+ **Status: 422** — Use when the data format is correct but business logic validation fails.
172
323
 
173
- res.paginate(users, "Users fetched", {
174
- page: 1,
175
- limit: 10,
176
- total: total,
324
+ ```js
325
+ res.unprocessable("Cannot process this request", {
326
+ age: "Must be at least 18 years old",
327
+ });
328
+
329
+ // Response:
330
+ // { success: false, message: "Cannot process this request", data: null, errors: { age: "..." }, meta: { statusCode: 422, ... } }
331
+ ```
332
+
333
+ ---
334
+
335
+ #### `res.tooManyRequests(message?, retryAfter?)`
336
+
337
+ **Status: 429** — Use when a client exceeds the rate limit.
338
+
339
+ ```js
340
+ // Basic usage
341
+ res.tooManyRequests("Too many requests, please slow down");
342
+
343
+ // With retryAfter in seconds — sets the Retry-After header automatically
344
+ res.tooManyRequests("Rate limit exceeded", 60);
345
+
346
+ // Response:
347
+ // Header: Retry-After: 60
348
+ // { success: false, message: "Rate limit exceeded", data: null, errors: null, meta: { statusCode: 429, ... } }
349
+ ```
350
+
351
+ ---
352
+
353
+ #### `res.serverError(message?)`
354
+
355
+ **Status: 500** — Use when an unexpected server-side error occurs.
356
+
357
+ ```js
358
+ try {
359
+ await someRiskyOperation();
360
+ } catch (err) {
361
+ console.error(err);
362
+ return res.serverError("Something went wrong, please try again later");
363
+ }
364
+
365
+ // Response:
366
+ // { success: false, message: "Something went wrong, please try again later", data: null, errors: null, meta: { statusCode: 500, ... } }
367
+ ```
368
+
369
+ ---
370
+
371
+ #### `res.error(message?, statusCode?, errors?)`
372
+
373
+ **Status: custom** — Use when you need a custom error status code.
374
+
375
+ ```js
376
+ res.error("Gone", 410);
377
+ res.error("Validation failed", 400, { field: "required" });
378
+ ```
379
+
380
+ ---
381
+
382
+ ### 📄 Pagination — `res.paginate(data, message?, pagination?)`
383
+
384
+ Use for returning paginated lists with full pagination metadata.
385
+
386
+ ```js
387
+ router.get("/users", async (req, res) => {
388
+ const page = parseInt(req.query.page) || 1;
389
+ const limit = parseInt(req.query.limit) || 10;
390
+ const skip = (page - 1) * limit;
391
+
392
+ const [users, total] = await Promise.all([
393
+ User.find().skip(skip).limit(limit),
394
+ User.countDocuments(),
395
+ ]);
396
+
397
+ return res.paginate(users, "Users fetched", { page, limit, total });
398
+ });
399
+
400
+ // Response:
401
+ // {
402
+ // success: true,
403
+ // message: "Users fetched",
404
+ // data: [...users],
405
+ // meta: {
406
+ // timestamp: "...",
407
+ // statusCode: 200,
408
+ // pagination: {
409
+ // page: 1, limit: 10, total: 100,
410
+ // totalPages: 10,
411
+ // hasNextPage: true,
412
+ // hasPrevPage: false
413
+ // }
414
+ // }
415
+ // }
416
+ ```
417
+
418
+ | Pagination Field | Type | Description |
419
+ | ---------------- | -------- | ------------------------- |
420
+ | `page` | `number` | Current page number |
421
+ | `limit` | `number` | Number of items per page |
422
+ | `total` | `number` | Total number of documents |
423
+
424
+ ---
425
+
426
+ ### ⚡ asyncHandler
427
+
428
+ Eliminates try-catch boilerplate from every async route.
429
+ Errors are automatically forwarded to Express's `next(err)`.
430
+
431
+ ```js
432
+ const { asyncHandler } = require("node-responder");
433
+
434
+ // ❌ Before — repetitive try-catch in every route:
435
+ router.get("/users", async (req, res) => {
436
+ try {
437
+ const users = await User.find();
438
+ res.ok(users);
439
+ } catch (err) {
440
+ res.serverError(err.message);
441
+ }
442
+ });
443
+
444
+ // ✅ After — clean and concise with asyncHandler:
445
+ router.get(
446
+ "/users",
447
+ asyncHandler(async (req, res) => {
448
+ const users = await User.find(); // errors automatically go to next(err)
449
+ res.ok(users);
450
+ }),
451
+ );
452
+
453
+ // Catch all errors in one global error handler:
454
+ app.use((err, req, res, next) => {
455
+ console.error(err);
456
+ res.serverError(err.message);
177
457
  });
178
458
  ```
179
459
 
180
460
  ---
181
461
 
182
- ## 🔧 Real-World Example (MERN)
462
+ ### 📝 Logger `{ logger: true }`
463
+
464
+ Logs every incoming request to the terminal with method, URL, status code, and response time.
465
+
466
+ ```js
467
+ app.use(responder({ logger: true }));
468
+ ```
469
+
470
+ Terminal output:
471
+
472
+ ```
473
+ GET /api/users 200 23ms ✔
474
+ POST /api/users 201 45ms ✔
475
+ GET /api/users/abc123 404 12ms ✖
476
+ POST /api/auth/login 401 8ms ✖
477
+ DELETE /api/products/999 500 5ms ✖
478
+ ```
479
+
480
+ **Tip:** Enable logger in development, disable in production:
481
+
482
+ ```js
483
+ app.use(
484
+ responder({
485
+ logger: process.env.NODE_ENV !== "production",
486
+ }),
487
+ );
488
+ ```
489
+
490
+ ---
491
+
492
+ ## 🔧 Real-World MERN Example
183
493
 
184
494
  ```js
185
495
  const express = require("express");
186
496
  const responder = require("node-responder");
187
- const router = express.Router();
497
+ const { asyncHandler } = require("node-responder");
188
498
 
189
- router.use(responder());
499
+ const router = express.Router();
500
+ router.use(responder({ logger: true }));
190
501
 
191
- // GET all users with pagination
192
- router.get("/", async (req, res) => {
193
- try {
502
+ // GET /api/users?page=1&limit=10
503
+ router.get(
504
+ "/",
505
+ asyncHandler(async (req, res) => {
194
506
  const page = parseInt(req.query.page) || 1;
195
507
  const limit = parseInt(req.query.limit) || 10;
196
508
  const skip = (page - 1) * limit;
197
509
 
198
510
  const [users, total] = await Promise.all([
199
- User.find().skip(skip).limit(limit),
511
+ User.find().skip(skip).limit(limit).select("-password"),
200
512
  User.countDocuments(),
201
513
  ]);
202
514
 
203
515
  return res.paginate(users, "Users fetched", { page, limit, total });
204
- } catch (err) {
205
- return res.serverError("Failed to fetch users");
206
- }
207
- });
208
-
209
- // POST create user
210
- router.post("/", async (req, res) => {
211
- try {
212
- const { name, email } = req.body;
213
-
214
- if (!name || !email) {
215
- return res.badRequest("Validation failed", {
216
- name: !name ? "Name is required" : null,
217
- email: !email ? "Email is required" : null,
218
- });
516
+ }),
517
+ );
518
+
519
+ // GET /api/users/:id
520
+ router.get(
521
+ "/:id",
522
+ asyncHandler(async (req, res) => {
523
+ const user = await User.findById(req.params.id).select("-password");
524
+ if (!user) return res.notFound("User not found");
525
+ return res.ok(user, "User fetched");
526
+ }),
527
+ );
528
+
529
+ // POST /api/users
530
+ router.post(
531
+ "/",
532
+ asyncHandler(async (req, res) => {
533
+ const { name, email, password } = req.body;
534
+
535
+ const errors = {};
536
+ if (!name) errors.name = "Name is required";
537
+ if (!email) errors.email = "Email is required";
538
+ if (!password) errors.password = "Password is required";
539
+
540
+ if (Object.keys(errors).length > 0) {
541
+ return res.badRequest("Validation failed", errors);
219
542
  }
220
543
 
221
544
  const exists = await User.findOne({ email });
222
545
  if (exists) return res.conflict("Email already registered");
223
546
 
224
- const user = await User.create({ name, email });
225
- return res.created(user, "User registered successfully");
226
- } catch (err) {
227
- return res.serverError();
228
- }
229
- });
547
+ const user = await User.create({ name, email, password });
548
+
549
+ return res.created(
550
+ { id: user._id, name: user.name, email: user.email },
551
+ "Account created successfully",
552
+ );
553
+ }),
554
+ );
555
+
556
+ // PUT /api/users/:id
557
+ router.put(
558
+ "/:id",
559
+ asyncHandler(async (req, res) => {
560
+ const user = await User.findByIdAndUpdate(req.params.id, req.body, {
561
+ new: true,
562
+ });
563
+ if (!user) return res.notFound("User not found");
564
+ return res.ok(user, "User updated successfully");
565
+ }),
566
+ );
567
+
568
+ // DELETE /api/users/:id
569
+ router.delete(
570
+ "/:id",
571
+ asyncHandler(async (req, res) => {
572
+ const user = await User.findByIdAndDelete(req.params.id);
573
+ if (!user) return res.notFound("User not found");
574
+ return res.noContent();
575
+ }),
576
+ );
577
+
578
+ module.exports = router;
230
579
  ```
231
580
 
232
581
  ---
@@ -235,19 +584,46 @@ router.post("/", async (req, res) => {
235
584
 
236
585
  ```ts
237
586
  import express from "express";
238
- import responder from "node-responder";
587
+ import responder, { asyncHandler } from "node-responder";
239
588
 
240
589
  const app = express();
241
- app.use(responder());
590
+ app.use(express.json());
591
+ app.use(responder({ logger: true }));
592
+
593
+ app.get(
594
+ "/users",
595
+ asyncHandler(async (req, res) => {
596
+ const users = await User.find();
597
+ res.ok(users, "Users fetched");
598
+ }),
599
+ );
600
+
601
+ // Global error handler
602
+ app.use(
603
+ (
604
+ err: Error,
605
+ req: express.Request,
606
+ res: express.Response,
607
+ next: express.NextFunction,
608
+ ) => {
609
+ console.error(err);
610
+ res.serverError(err.message);
611
+ },
612
+ );
242
613
 
243
- app.get("/users", async (req, res) => {
244
- const users = await User.find();
245
- res.ok(users, "Users fetched");
246
- });
614
+ app.listen(3000);
247
615
  ```
248
616
 
249
617
  ---
250
618
 
619
+ ## 🔗 Links
620
+
621
+ - [npm](https://www.npmjs.com/package/node-responder)
622
+ - [GitHub](https://github.com/hammadsadi/node-responder)
623
+ - [Report an Issue](https://github.com/hammadsadi/node-responder/issues)
624
+
625
+ ---
626
+
251
627
  ## 📄 License
252
628
 
253
629
  MIT © [Hammad Sadi](https://github.com/hammadsadi)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-responder",
3
- "version": "1.0.0",
3
+ "version": "1.2.1",
4
4
  "description": "Modern, standardized API response middleware for Express.js — with TypeScript support, pagination, and shorthand methods.",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -56,4 +56,4 @@
56
56
  "engines": {
57
57
  "node": ">=14.0.0"
58
58
  }
59
- }
59
+ }
package/src/index.js CHANGED
@@ -3,140 +3,282 @@
3
3
  /**
4
4
  * node-responder
5
5
  * Standardized, modern API response middleware for Express.js
6
- * @author Hammad Sadi
6
+ * @version 1.1.0
7
7
  * @license MIT
8
8
  */
9
9
 
10
- /**
11
- * Creates a success response
12
- */
13
- function successResponse(
14
- res,
15
- data = null,
16
- message = "Success",
17
- statusCode = 200,
18
- ) {
19
- return res.status(statusCode).json({
10
+ // ANSI Colors
11
+
12
+ const COLORS = {
13
+ reset: "\x1b[0m",
14
+ dim: "\x1b[2m",
15
+ bold: "\x1b[1m",
16
+ green: "\x1b[32m",
17
+ yellow: "\x1b[33m",
18
+ red: "\x1b[31m",
19
+ cyan: "\x1b[36m",
20
+ };
21
+
22
+ const isTTY = () => process.stdout && process.stdout.isTTY === true;
23
+
24
+ const colorize = (str, color) =>
25
+ isTTY() ? `${color}${str}${COLORS.reset}` : str;
26
+
27
+ const colorStatus = (code) => {
28
+ if (code >= 500) return colorize(code, COLORS.red);
29
+ if (code >= 400) return colorize(code, COLORS.yellow);
30
+ if (code >= 300) return colorize(code, COLORS.cyan);
31
+ return colorize(code, COLORS.green);
32
+ };
33
+
34
+ const colorMethod = (method) => {
35
+ const m = (method || "UNKNOWN").toUpperCase().padEnd(7);
36
+ return colorize(m, COLORS.cyan);
37
+ };
38
+
39
+ const logRequest = (req, statusCode, startTime) => {
40
+ try {
41
+ const ms = Date.now() - startTime;
42
+ const method = colorMethod(req.method);
43
+ const url = (req.originalUrl || req.url || "/").padEnd(35);
44
+ const status = colorStatus(statusCode);
45
+ const time = colorize(`${ms}ms`, COLORS.dim);
46
+ const icon =
47
+ statusCode >= 400
48
+ ? colorize("✖", COLORS.red)
49
+ : colorize("✔", COLORS.green);
50
+ process.stdout.write(` ${method} ${url} ${status} ${time} ${icon}\n`);
51
+ } catch (_) {}
52
+ };
53
+
54
+ // Validation helpers
55
+
56
+ const safeMessage = (val, fallback) => {
57
+ if (val === undefined || val === null) return fallback;
58
+ return String(val);
59
+ };
60
+
61
+ const safeStatus = (val, fallback) => {
62
+ const n = Number(val);
63
+ return Number.isFinite(n) && n >= 100 && n <= 599 ? n : fallback;
64
+ };
65
+
66
+ // Core response builders
67
+
68
+ const successResponse = (res, data, message, statusCode) => {
69
+ const code = safeStatus(statusCode, 200);
70
+ const msg = safeMessage(message, "Success");
71
+ const body = data === undefined ? null : data;
72
+
73
+ return res.status(code).json({
20
74
  success: true,
21
- message,
22
- data,
75
+ message: msg,
76
+ data: body,
23
77
  meta: {
24
78
  timestamp: new Date().toISOString(),
25
- statusCode,
79
+ statusCode: code,
26
80
  },
27
81
  });
28
- }
82
+ };
29
83
 
30
- /**
31
- * Creates an error response
32
- */
33
- function errorResponse(
34
- res,
35
- message = "Something went wrong",
36
- statusCode = 500,
37
- errors = null,
38
- ) {
39
- return res.status(statusCode).json({
84
+ const errorResponse = (res, message, statusCode, errors) => {
85
+ const code = safeStatus(statusCode, 500);
86
+ const msg = safeMessage(message, "Something went wrong");
87
+ const errs = errors === undefined || errors === null ? null : errors;
88
+
89
+ return res.status(code).json({
40
90
  success: false,
41
- message,
91
+ message: msg,
42
92
  data: null,
43
- errors,
93
+ errors: errs,
44
94
  meta: {
45
95
  timestamp: new Date().toISOString(),
46
- statusCode,
96
+ statusCode: code,
47
97
  },
48
98
  });
49
- }
99
+ };
50
100
 
51
- /**
52
- * Creates a paginated response
53
- */
54
- function paginatedResponse(
55
- res,
56
- data = [],
57
- message = "Success",
58
- pagination = {},
59
- ) {
60
- const { page = 1, limit = 10, total = 0 } = pagination;
101
+ const paginatedResponse = (res, data, message, pagination) => {
102
+ const arr = Array.isArray(data) ? data : [];
103
+ const msg = safeMessage(message, "Success");
104
+ const p = pagination && typeof pagination === "object" ? pagination : {};
61
105
 
62
- const totalPages = Math.ceil(total / limit);
106
+ const page = Math.max(1, parseInt(p.page, 10) || 1);
107
+ const limit = Math.max(1, parseInt(p.limit, 10) || 10);
108
+ const total = Math.max(0, parseInt(p.total, 10) || 0);
109
+ const totalPages = limit > 0 ? Math.ceil(total / limit) : 0;
63
110
 
64
111
  return res.status(200).json({
65
112
  success: true,
66
- message,
67
- data,
113
+ message: msg,
114
+ data: arr,
68
115
  meta: {
69
116
  timestamp: new Date().toISOString(),
70
117
  statusCode: 200,
71
118
  pagination: {
72
- page: Number(page),
73
- limit: Number(limit),
74
- total: Number(total),
119
+ page,
120
+ limit,
121
+ total,
75
122
  totalPages,
76
123
  hasNextPage: page < totalPages,
77
124
  hasPrevPage: page > 1,
78
125
  },
79
126
  },
80
127
  });
81
- }
128
+ };
82
129
 
83
- /**
84
- * Express middleware — attaches helper methods to res object
85
- */
86
- function apiResponse() {
87
- return function (req, res, next) {
88
- // res.success(data, message, statusCode)
89
- res.success = function (
90
- data = null,
91
- message = "Success",
92
- statusCode = 200,
93
- ) {
94
- return successResponse(res, data, message, statusCode);
130
+ // asyncHandler
131
+
132
+ const asyncHandler = (fn) => {
133
+ if (typeof fn !== "function") {
134
+ throw new TypeError(
135
+ `[node-responder] asyncHandler expects a function, got "${typeof fn}"`,
136
+ );
137
+ }
138
+ return (req, res, next) => {
139
+ try {
140
+ const result = fn(req, res, next);
141
+ if (result && typeof result.catch === "function") {
142
+ result.catch(next);
143
+ }
144
+ } catch (err) {
145
+ next(err);
146
+ }
147
+ };
148
+ };
149
+
150
+ // Middleware factory
151
+
152
+ const responder = (options) => {
153
+ const opts = options && typeof options === "object" ? options : {};
154
+ const logger = opts.logger === true;
155
+
156
+ if (process.env.NODE_ENV !== "production") {
157
+ const known = ["logger"];
158
+ for (const key of Object.keys(opts)) {
159
+ if (!known.includes(key)) {
160
+ process.stderr.write(
161
+ `[node-responder] Unknown option "${key}" — valid options: ${known.join(", ")}\n`,
162
+ );
163
+ }
164
+ }
165
+ }
166
+
167
+ return (req, res, next) => {
168
+ const startTime = Date.now();
169
+
170
+ const log = (code) => {
171
+ if (logger) logRequest(req, code, startTime);
172
+ };
173
+
174
+ // 2xx Success
175
+
176
+ res.success = (data, message, statusCode) => {
177
+ const code = safeStatus(statusCode, 200);
178
+ log(code);
179
+ return successResponse(res, data, message, code);
95
180
  };
96
181
 
97
- // res.error(message, statusCode, errors)
98
- res.error = function (
99
- message = "Something went wrong",
100
- statusCode = 500,
101
- errors = null,
102
- ) {
103
- return errorResponse(res, message, statusCode, errors);
182
+ res.ok = (data, message) => {
183
+ log(200);
184
+ return successResponse(res, data, safeMessage(message, "Success"), 200);
104
185
  };
105
186
 
106
- // res.paginate(data, message, pagination)
107
- res.paginate = function (data = [], message = "Success", pagination = {}) {
108
- return paginatedResponse(res, data, message, pagination);
187
+ res.created = (data, message) => {
188
+ log(201);
189
+ return successResponse(
190
+ res,
191
+ data,
192
+ safeMessage(message, "Created successfully"),
193
+ 201,
194
+ );
195
+ };
196
+
197
+ res.noContent = () => {
198
+ log(204);
199
+ return res.status(204).send();
200
+ };
201
+
202
+ // 4xx / 5xx Error
203
+
204
+ res.error = (message, statusCode, errors) => {
205
+ const code = safeStatus(statusCode, 500);
206
+ log(code);
207
+ return errorResponse(res, message, code, errors);
109
208
  };
110
209
 
111
- // Shorthand methods
112
- res.ok = (data, message = "Success") =>
113
- successResponse(res, data, message, 200);
114
- res.created = (data, message = "Created successfully") =>
115
- successResponse(res, data, message, 201);
116
- res.noContent = () => res.status(204).send();
117
-
118
- res.badRequest = (message = "Bad request", errors = null) =>
119
- errorResponse(res, message, 400, errors);
120
- res.unauthorized = (message = "Unauthorized") =>
121
- errorResponse(res, message, 401);
122
- res.forbidden = (message = "Forbidden") => errorResponse(res, message, 403);
123
- res.notFound = (message = "Not found") => errorResponse(res, message, 404);
124
- res.conflict = (message = "Conflict") => errorResponse(res, message, 409);
125
- res.unprocessable = (message = "Unprocessable entity", errors = null) =>
126
- errorResponse(res, message, 422, errors);
127
- res.serverError = (message = "Internal server error") =>
128
- errorResponse(res, message, 500);
210
+ res.badRequest = (message, errors) => {
211
+ log(400);
212
+ return errorResponse(
213
+ res,
214
+ safeMessage(message, "Bad request"),
215
+ 400,
216
+ errors ?? null,
217
+ );
218
+ };
219
+
220
+ res.unauthorized = (message) => {
221
+ log(401);
222
+ return errorResponse(res, safeMessage(message, "Unauthorized"), 401);
223
+ };
224
+
225
+ res.forbidden = (message) => {
226
+ log(403);
227
+ return errorResponse(res, safeMessage(message, "Forbidden"), 403);
228
+ };
229
+
230
+ res.notFound = (message) => {
231
+ log(404);
232
+ return errorResponse(res, safeMessage(message, "Not found"), 404);
233
+ };
234
+
235
+ res.conflict = (message) => {
236
+ log(409);
237
+ return errorResponse(res, safeMessage(message, "Conflict"), 409);
238
+ };
239
+
240
+ res.unprocessable = (message, errors) => {
241
+ log(422);
242
+ return errorResponse(
243
+ res,
244
+ safeMessage(message, "Unprocessable entity"),
245
+ 422,
246
+ errors ?? null,
247
+ );
248
+ };
249
+
250
+ res.tooManyRequests = (message, retryAfter) => {
251
+ if (retryAfter != null) res.set("Retry-After", String(retryAfter));
252
+ log(429);
253
+ return errorResponse(res, safeMessage(message, "Too many requests"), 429);
254
+ };
255
+
256
+ res.serverError = (message) => {
257
+ log(500);
258
+ return errorResponse(
259
+ res,
260
+ safeMessage(message, "Internal server error"),
261
+ 500,
262
+ );
263
+ };
264
+
265
+ // Pagination
266
+
267
+ res.paginate = (data, message, pagination) => {
268
+ log(200);
269
+ return paginatedResponse(res, data, message, pagination);
270
+ };
129
271
 
130
272
  next();
131
273
  };
132
- }
274
+ };
275
+
276
+ // Exports
133
277
 
134
- // Named exports
135
- module.exports = apiResponse;
136
- module.exports.apiResponse = apiResponse;
278
+ module.exports = responder;
279
+ module.exports.default = responder;
280
+ module.exports.responder = responder;
281
+ module.exports.asyncHandler = asyncHandler;
137
282
  module.exports.successResponse = successResponse;
138
283
  module.exports.errorResponse = errorResponse;
139
284
  module.exports.paginatedResponse = paginatedResponse;
140
-
141
- // ESM default export support
142
- module.exports.default = apiResponse;
package/types/index.d.ts CHANGED
@@ -1,9 +1,21 @@
1
- import { Request, Response, NextFunction, RequestHandler } from "express";
1
+ import { RequestHandler, Request, Response, NextFunction } from "express";
2
2
 
3
- export interface Pagination {
4
- page?: number;
5
- limit?: number;
6
- total?: number;
3
+ // Options
4
+
5
+ export interface ResponderOptions {
6
+ /**
7
+ * When true, logs every request to stdout with method, URL, status, and response time.
8
+ * @default false
9
+ */
10
+ logger?: boolean;
11
+ }
12
+
13
+ // Pagination
14
+
15
+ export interface PaginationInput {
16
+ page?: number | string;
17
+ limit?: number | string;
18
+ total?: number | string;
7
19
  }
8
20
 
9
21
  export interface PaginationMeta {
@@ -15,13 +27,15 @@ export interface PaginationMeta {
15
27
  hasPrevPage: boolean;
16
28
  }
17
29
 
30
+ // Response shapes
31
+
18
32
  export interface ApiMeta {
19
33
  timestamp: string;
20
34
  statusCode: number;
21
35
  pagination?: PaginationMeta;
22
36
  }
23
37
 
24
- export interface ApiSuccessResponse<T = any> {
38
+ export interface ApiSuccessResponse<T = unknown> {
25
39
  success: true;
26
40
  message: string;
27
41
  data: T;
@@ -32,40 +46,42 @@ export interface ApiErrorResponse {
32
46
  success: false;
33
47
  message: string;
34
48
  data: null;
35
- errors: any | null;
49
+ errors: unknown | null;
36
50
  meta: ApiMeta;
37
51
  }
38
52
 
53
+ // Express augmentation
54
+
39
55
  declare global {
40
56
  namespace Express {
41
57
  interface Response {
42
- /** Send a success response */
43
- success<T = any>(
58
+ // Generic
59
+ /** Send a success response with optional data, message, and status code */
60
+ success<T = unknown>(
44
61
  data?: T,
45
62
  message?: string,
46
63
  statusCode?: number,
47
64
  ): Response;
48
-
49
- /** Send an error response */
50
- error(message?: string, statusCode?: number, errors?: any): Response;
51
-
52
- /** Send a paginated response */
53
- paginate<T = any>(
65
+ /** Send an error response with optional message, status code, and errors */
66
+ error(message?: string, statusCode?: number, errors?: unknown): Response;
67
+ /** Send a paginated success response */
68
+ paginate<T = unknown>(
54
69
  data?: T[],
55
70
  message?: string,
56
- pagination?: Pagination,
71
+ pagination?: PaginationInput,
57
72
  ): Response;
58
73
 
59
- // Shorthand methods
74
+ // 2xx
60
75
  /** 200 OK */
61
- ok<T = any>(data?: T, message?: string): Response;
76
+ ok<T = unknown>(data?: T, message?: string): Response;
62
77
  /** 201 Created */
63
- created<T = any>(data?: T, message?: string): Response;
78
+ created<T = unknown>(data?: T, message?: string): Response;
64
79
  /** 204 No Content */
65
80
  noContent(): Response;
66
81
 
82
+ // 4xx
67
83
  /** 400 Bad Request */
68
- badRequest(message?: string, errors?: any): Response;
84
+ badRequest(message?: string, errors?: unknown): Response;
69
85
  /** 401 Unauthorized */
70
86
  unauthorized(message?: string): Response;
71
87
  /** 403 Forbidden */
@@ -75,18 +91,34 @@ declare global {
75
91
  /** 409 Conflict */
76
92
  conflict(message?: string): Response;
77
93
  /** 422 Unprocessable Entity */
78
- unprocessable(message?: string, errors?: any): Response;
94
+ unprocessable(message?: string, errors?: unknown): Response;
95
+ /** 429 Too Many Requests — optionally sets Retry-After header */
96
+ tooManyRequests(message?: string, retryAfter?: number | string): Response;
97
+
98
+ // 5xx
79
99
  /** 500 Internal Server Error */
80
100
  serverError(message?: string): Response;
81
101
  }
82
102
  }
83
103
  }
84
104
 
85
- export declare function apiResponse(): RequestHandler;
105
+ // Exported functions
106
+
107
+ /** Express middleware — attaches all response helpers to res */
108
+ export declare function responder(options?: ResponderOptions): RequestHandler;
109
+
110
+ /** Wraps an async route handler and forwards errors to next() automatically */
111
+ export declare function asyncHandler(
112
+ fn: (
113
+ req: Request,
114
+ res: Response,
115
+ next: NextFunction,
116
+ ) => Promise<unknown> | unknown,
117
+ ): RequestHandler;
86
118
 
87
119
  export declare function successResponse(
88
120
  res: Response,
89
- data?: any,
121
+ data?: unknown,
90
122
  message?: string,
91
123
  statusCode?: number,
92
124
  ): Response;
@@ -95,14 +127,14 @@ export declare function errorResponse(
95
127
  res: Response,
96
128
  message?: string,
97
129
  statusCode?: number,
98
- errors?: any,
130
+ errors?: unknown,
99
131
  ): Response;
100
132
 
101
133
  export declare function paginatedResponse(
102
134
  res: Response,
103
- data?: any[],
135
+ data?: unknown[],
104
136
  message?: string,
105
- pagination?: Pagination,
137
+ pagination?: PaginationInput,
106
138
  ): Response;
107
139
 
108
- export default apiResponse;
140
+ export default responder;