@naman_deep_singh/http-response 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.
Files changed (58) hide show
  1. package/README.md +189 -0
  2. package/dist/cjs/adapters/express/ExpressResponder.d.ts +18 -0
  3. package/dist/cjs/adapters/express/ExpressResponder.js +46 -0
  4. package/dist/cjs/constants/httpStatus.d.ts +54 -0
  5. package/dist/cjs/constants/httpStatus.js +34 -0
  6. package/dist/cjs/core/BaseResponder.d.ts +31 -0
  7. package/dist/cjs/core/BaseResponder.js +116 -0
  8. package/dist/cjs/core/config.d.ts +15 -0
  9. package/dist/cjs/core/config.js +15 -0
  10. package/dist/cjs/core/factory.d.ts +3 -0
  11. package/dist/cjs/core/factory.js +10 -0
  12. package/dist/cjs/core/types.d.ts +32 -0
  13. package/dist/cjs/core/types.js +2 -0
  14. package/dist/cjs/index.d.ts +8 -0
  15. package/dist/cjs/index.js +32 -0
  16. package/dist/cjs/legacy.d.ts +19 -0
  17. package/dist/cjs/legacy.js +23 -0
  18. package/dist/cjs/middleware/express/expressMiddleware.d.ts +3 -0
  19. package/dist/cjs/middleware/express/expressMiddleware.js +13 -0
  20. package/dist/esm/adapters/express/ExpressResponder.d.ts +18 -0
  21. package/dist/esm/adapters/express/ExpressResponder.js +42 -0
  22. package/dist/esm/constants/httpStatus.d.ts +54 -0
  23. package/dist/esm/constants/httpStatus.js +31 -0
  24. package/dist/esm/core/BaseResponder.d.ts +31 -0
  25. package/dist/esm/core/BaseResponder.js +112 -0
  26. package/dist/esm/core/config.d.ts +15 -0
  27. package/dist/esm/core/config.js +12 -0
  28. package/dist/esm/core/factory.d.ts +3 -0
  29. package/dist/esm/core/factory.js +6 -0
  30. package/dist/esm/core/types.d.ts +32 -0
  31. package/dist/esm/core/types.js +1 -0
  32. package/dist/esm/index.d.ts +8 -0
  33. package/dist/esm/index.js +10 -0
  34. package/dist/esm/legacy.d.ts +19 -0
  35. package/dist/esm/legacy.js +18 -0
  36. package/dist/esm/middleware/express/expressMiddleware.d.ts +3 -0
  37. package/dist/esm/middleware/express/expressMiddleware.js +9 -0
  38. package/dist/types/adapters/express/ExpressResponder.d.ts +18 -0
  39. package/dist/types/constants/httpStatus.d.ts +54 -0
  40. package/dist/types/core/BaseResponder.d.ts +31 -0
  41. package/dist/types/core/config.d.ts +15 -0
  42. package/dist/types/core/factory.d.ts +3 -0
  43. package/dist/types/core/types.d.ts +32 -0
  44. package/dist/types/index.d.ts +8 -0
  45. package/dist/types/legacy.d.ts +19 -0
  46. package/dist/types/middleware/express/expressMiddleware.d.ts +3 -0
  47. package/package.json +53 -0
  48. package/src/adapters/express/ExpressResponder.ts +64 -0
  49. package/src/constants/httpStatus.ts +42 -0
  50. package/src/core/BaseResponder.ts +198 -0
  51. package/src/core/config.ts +29 -0
  52. package/src/core/factory.ts +11 -0
  53. package/src/core/types.ts +34 -0
  54. package/src/index.ts +13 -0
  55. package/src/middleware/express/expressMiddleware.ts +14 -0
  56. package/tsconfig.base.json +11 -0
  57. package/tsconfig.cjs.json +9 -0
  58. package/tsconfig.esm.json +9 -0
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@naman_deep_singh/http-response",
3
+ "version": "3.0.0",
4
+ "description": "TypeScript utilities for standardized API responses",
5
+ "author": "Naman Deep Singh",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/cjs/index.js",
9
+ "module": "./dist/esm/index.js",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/cjs/index.js",
14
+ "types": "./dist/types/index.d.ts"
15
+ },
16
+ "./express": {
17
+ "import": "./dist/esm/adapters/express/index.js",
18
+ "require": "./dist/cjs/adapters/express/index.js",
19
+ "types": "./dist/types/adapters/express/index.d.ts"
20
+ },
21
+ "./legacy": {
22
+ "import": "./dist/esm/legacy/index.js",
23
+ "require": "./dist/cjs/legacy/index.js",
24
+ "types": "./dist/types/legacy/index.d.ts"
25
+ }
26
+ },
27
+ "sideEffects": false,
28
+ "keywords": [
29
+ "response",
30
+ "http",
31
+ "api",
32
+ "utils",
33
+ "express",
34
+ "typescript"
35
+ ],
36
+ "peerDependencies": {
37
+ "express": "^5.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/express": "^5.0.5",
41
+ "express": "^5.1.0",
42
+ "typescript": "^5.9.3",
43
+ "rimraf": "^5.0.5"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "pnpm run build:types && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
50
+ "build:types": "tsc -p tsconfig.base.json --emitDeclarationOnly --outDir dist/types",
51
+ "clean": "rimraf dist"
52
+ }
53
+ }
@@ -0,0 +1,64 @@
1
+ import type { Response } from 'express'
2
+ import { BaseResponder } from '../../core/BaseResponder'
3
+ import type { ResponderConfig } from '../../core/config'
4
+
5
+ export class ExpressResponder<P = unknown> extends BaseResponder<P> {
6
+ constructor(
7
+ cfg: Partial<ResponderConfig> | undefined,
8
+ private readonly res: Response,
9
+ ) {
10
+ // attach sender which calls res.status().json()
11
+ super(cfg, (status, body) => res.status(status).json(body))
12
+ }
13
+
14
+ // convenience methods that return void for middleware/controller ergonomics
15
+ okAndSend(data?: P, message?: string) {
16
+ void this.ok(data, message)
17
+ }
18
+
19
+ createdAndSend(data?: P, message?: string) {
20
+ void this.created(data, message)
21
+ }
22
+
23
+ badRequestAndSend(message?: string, error?: unknown) {
24
+ void this.badRequest(message, error)
25
+ }
26
+
27
+ unauthorizedAndSend(message?: string) {
28
+ void this.unauthorized(message)
29
+ }
30
+
31
+ forbiddenAndSend(message?: string) {
32
+ void this.forbidden(message)
33
+ }
34
+
35
+ notFoundAndSend(message?: string) {
36
+ void this.notFound(message)
37
+ }
38
+
39
+ conflictAndSend(message?: string) {
40
+ void this.conflict(message)
41
+ }
42
+
43
+ unprocessableEntityAndSend(message?: string, error?: unknown) {
44
+ void this.unprocessableEntity(message, error)
45
+ }
46
+
47
+ tooManyRequestsAndSend(message?: string) {
48
+ void this.tooManyRequests(message)
49
+ }
50
+
51
+ serverErrorAndSend(message?: string, error?: unknown) {
52
+ void this.serverError(message, error)
53
+ }
54
+
55
+ paginateAndSend(
56
+ data: P[],
57
+ page: number,
58
+ limit: number,
59
+ total: number,
60
+ message?: string,
61
+ ) {
62
+ void this.paginate(data, page, limit, total, message)
63
+ }
64
+ }
@@ -0,0 +1,42 @@
1
+ const SUCCESS = Object.freeze({
2
+ OK: 200,
3
+ CREATED: 201,
4
+ ACCEPTED: 202,
5
+ NO_CONTENT: 204,
6
+ } as const)
7
+
8
+ const REDIRECTION = Object.freeze({
9
+ NOT_MODIFIED: 304,
10
+ } as const)
11
+
12
+ const CLIENT_ERROR = Object.freeze({
13
+ BAD_REQUEST: 400,
14
+ UNAUTHORIZED: 401,
15
+ FORBIDDEN: 403,
16
+ NOT_FOUND: 404,
17
+ METHOD_NOT_ALLOWED: 405,
18
+ CONFLICT: 409,
19
+ UNPROCESSABLE_ENTITY: 422,
20
+ TOO_MANY_REQUESTS: 429,
21
+ } as const)
22
+
23
+ const SERVER_ERROR = Object.freeze({
24
+ INTERNAL_SERVER_ERROR: 500,
25
+ NOT_IMPLEMENTED: 501,
26
+ BAD_GATEWAY: 502,
27
+ SERVICE_UNAVAILABLE: 503,
28
+ } as const)
29
+
30
+ export const HTTP_STATUS = Object.freeze({
31
+ SUCCESS,
32
+ REDIRECTION,
33
+ CLIENT_ERROR,
34
+ SERVER_ERROR,
35
+ } as const)
36
+
37
+ // flattened union type if needed:
38
+ export type HttpStatusCode =
39
+ | (typeof SUCCESS)[keyof typeof SUCCESS]
40
+ | (typeof REDIRECTION)[keyof typeof REDIRECTION]
41
+ | (typeof CLIENT_ERROR)[keyof typeof CLIENT_ERROR]
42
+ | (typeof SERVER_ERROR)[keyof typeof SERVER_ERROR]
@@ -0,0 +1,198 @@
1
+ import { HTTP_STATUS } from '../constants/httpStatus'
2
+
3
+ import { type ResponderConfig, defaultConfig } from './config'
4
+
5
+ import type { PaginationMeta, ResponseEnvelope, Sender } from './types'
6
+
7
+ export class BaseResponder<P = unknown, M = PaginationMeta> {
8
+ protected readonly cfg: ResponderConfig
9
+ protected sender?: Sender
10
+
11
+ constructor(cfg?: Partial<ResponderConfig>, sender?: Sender) {
12
+ this.cfg = { ...defaultConfig, ...(cfg ?? {}) }
13
+ this.sender = sender
14
+ }
15
+
16
+ attachSender(sender: Sender) {
17
+ this.sender = sender
18
+ }
19
+
20
+ protected normalizeError(err: unknown): {
21
+ message: string
22
+ code?: string
23
+ details?: unknown
24
+ } {
25
+ // errors-utils AppError compatibility
26
+ if (typeof err === 'object' && err !== null) {
27
+ const e = err as Record<string, unknown>
28
+
29
+ if (typeof e.message === 'string') {
30
+ return {
31
+ message: e.message,
32
+ code: typeof e.code === 'string' ? e.code : undefined,
33
+ details: e.details,
34
+ }
35
+ }
36
+ }
37
+
38
+ if (err instanceof Error) {
39
+ return { message: err.message }
40
+ }
41
+
42
+ if (typeof err === 'string') {
43
+ return { message: err }
44
+ }
45
+
46
+ return {
47
+ message: 'Internal server error',
48
+ details: err,
49
+ }
50
+ }
51
+
52
+ protected buildEnvelope(
53
+ data?: P,
54
+ message?: string,
55
+ error?: unknown,
56
+ meta?: M,
57
+ ) {
58
+ const env: ResponseEnvelope<P, M> = {
59
+ success: !error,
60
+ message: message ?? (error ? 'Error' : undefined),
61
+ data: error ? undefined : data,
62
+ error: error ? this.normalizeError(error) : null,
63
+ meta: meta ?? null,
64
+ }
65
+
66
+ if (this.cfg.timestamp) {
67
+ env.meta = {
68
+ ...(env.meta ?? {}),
69
+ timestamp: new Date().toISOString(),
70
+ } as M
71
+ }
72
+
73
+ if (this.cfg.extra) {
74
+ Object.assign(env as Record<string, unknown>, this.cfg.extra)
75
+ }
76
+
77
+ return env
78
+ }
79
+
80
+ protected send(status: number, envelope: ResponseEnvelope<P, M>) {
81
+ if (!this.sender) return { status, body: envelope }
82
+ return this.sender(status, envelope)
83
+ }
84
+
85
+ /** -----------------------------
86
+ * Standard REST Response Helpers
87
+ * ----------------------------- */
88
+ ok(data?: P, message = 'Success') {
89
+ return this.send(HTTP_STATUS.SUCCESS.OK, this.buildEnvelope(data, message))
90
+ }
91
+
92
+ created(data?: P, message = 'Created successfully') {
93
+ return this.send(
94
+ HTTP_STATUS.SUCCESS.CREATED,
95
+ this.buildEnvelope(data, message),
96
+ )
97
+ }
98
+
99
+ noContent(message = 'No Content') {
100
+ return this.send(
101
+ HTTP_STATUS.SUCCESS.NO_CONTENT,
102
+ this.buildEnvelope(undefined, message),
103
+ )
104
+ }
105
+
106
+ badRequest(message = 'Bad request', error?: unknown) {
107
+ return this.send(
108
+ HTTP_STATUS.CLIENT_ERROR.BAD_REQUEST,
109
+ this.buildEnvelope(undefined, message, error),
110
+ )
111
+ }
112
+
113
+ unauthorized(message = 'Unauthorized') {
114
+ return this.send(
115
+ HTTP_STATUS.CLIENT_ERROR.UNAUTHORIZED,
116
+ this.buildEnvelope(undefined, message),
117
+ )
118
+ }
119
+
120
+ forbidden(message = 'Forbidden') {
121
+ return this.send(
122
+ HTTP_STATUS.CLIENT_ERROR.FORBIDDEN,
123
+ this.buildEnvelope(undefined, message),
124
+ )
125
+ }
126
+
127
+ notFound(message = 'Not found') {
128
+ return this.send(
129
+ HTTP_STATUS.CLIENT_ERROR.NOT_FOUND,
130
+ this.buildEnvelope(undefined, message),
131
+ )
132
+ }
133
+
134
+ conflict(message = 'Conflict') {
135
+ return this.send(
136
+ HTTP_STATUS.CLIENT_ERROR.CONFLICT,
137
+ this.buildEnvelope(undefined, message),
138
+ )
139
+ }
140
+
141
+ unprocessableEntity(message = 'Unprocessable Entity', error?: unknown) {
142
+ return this.send(
143
+ HTTP_STATUS.CLIENT_ERROR.UNPROCESSABLE_ENTITY,
144
+ this.buildEnvelope(undefined, message, error),
145
+ )
146
+ }
147
+
148
+ tooManyRequests(message = 'Too Many Requests') {
149
+ return this.send(
150
+ HTTP_STATUS.CLIENT_ERROR.TOO_MANY_REQUESTS,
151
+ this.buildEnvelope(undefined, message),
152
+ )
153
+ }
154
+
155
+ serverError(message = 'Internal server error', error?: unknown) {
156
+ return this.send(
157
+ HTTP_STATUS.SERVER_ERROR.INTERNAL_SERVER_ERROR,
158
+ this.buildEnvelope(undefined, message, error),
159
+ )
160
+ }
161
+
162
+ paginate(
163
+ data: P[],
164
+ page: number,
165
+ limit: number,
166
+ total: number,
167
+ message = 'Success',
168
+ ) {
169
+ const totalPages = Math.max(1, Math.ceil(total / limit))
170
+ const offset = (page - 1) * limit
171
+
172
+ const pagination: PaginationMeta = {
173
+ page,
174
+ limit,
175
+ total,
176
+ totalPages,
177
+ offset,
178
+ hasNext: page < totalPages,
179
+ hasPrev: page > 1,
180
+ }
181
+
182
+ return this.send(
183
+ HTTP_STATUS.SUCCESS.OK,
184
+ this.buildEnvelope(data as any, message, undefined, pagination as any),
185
+ )
186
+ }
187
+
188
+ paginateOffset(
189
+ data: P[],
190
+ offset: number,
191
+ limit: number,
192
+ total: number,
193
+ message = 'Success',
194
+ ) {
195
+ const page = Math.floor(offset / limit) + 1
196
+ return this.paginate(data, page, limit, total, message)
197
+ }
198
+ }
@@ -0,0 +1,29 @@
1
+ import type { PlainObject } from './types'
2
+
3
+ export type EnvelopeKeys = {
4
+ success?: string
5
+ message?: string
6
+ data?: string
7
+ error?: string
8
+ meta?: string
9
+ }
10
+
11
+ export type ResponderConfig = {
12
+ envelopeKeys?: EnvelopeKeys
13
+ defaultStatus?: number
14
+ timestamp?: boolean
15
+ extra?: PlainObject | null
16
+ }
17
+
18
+ export const defaultConfig: ResponderConfig = {
19
+ envelopeKeys: {
20
+ success: 'success',
21
+ message: 'message',
22
+ data: 'data',
23
+ error: 'error',
24
+ meta: 'meta',
25
+ },
26
+ defaultStatus: 200,
27
+ timestamp: false,
28
+ extra: null,
29
+ }
@@ -0,0 +1,11 @@
1
+ import { ExpressResponder } from '../adapters/express/ExpressResponder'
2
+
3
+ import type { ResponderConfig } from './config'
4
+
5
+ export const createResponderFactory = (cfg?: Partial<ResponderConfig>) => {
6
+ return <P = unknown, _M = Record<string, unknown>>(
7
+ res: import('express').Response,
8
+ ) => {
9
+ return new ExpressResponder<P>(cfg, res)
10
+ }
11
+ }
@@ -0,0 +1,34 @@
1
+ export type PlainObject = Record<string, unknown>
2
+
3
+ export type ErrorShape = {
4
+ code?: string
5
+ message: string
6
+ details?: unknown
7
+ }
8
+
9
+ export type ResponseEnvelope<T = unknown, M = PlainObject> = {
10
+ success: boolean
11
+ message?: string
12
+ data?: T
13
+ error?: ErrorShape | null
14
+ meta?: M | null
15
+ }
16
+
17
+ export type TransportResult = { status: number; body: unknown }
18
+
19
+ export type Sender = (status: number, body: unknown) => Promise<any> | any
20
+
21
+ export interface PaginationMeta {
22
+ page: number
23
+ limit: number
24
+ total: number
25
+ totalPages: number
26
+ hasNext?: boolean
27
+ hasPrev?: boolean
28
+ offset?: number
29
+ links?: {
30
+ next?: string
31
+ prev?: string
32
+ self: string
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ // Core Responders
2
+ export { BaseResponder } from './core/BaseResponder'
3
+ export { createResponderFactory } from './core/factory'
4
+ export * from './core/types'
5
+ export * from './core/config'
6
+
7
+ // Adapters (Express)
8
+ export { ExpressResponder } from './adapters/express/ExpressResponder'
9
+ export { responderMiddleware } from './middleware/express/expressMiddleware'
10
+
11
+ // HTTP Status Constants
12
+ export { HTTP_STATUS } from './constants/httpStatus'
13
+ export type { HttpStatusCode } from './constants/httpStatus'
@@ -0,0 +1,14 @@
1
+ import type { RequestHandler } from 'express'
2
+ import type { ResponderConfig } from '../../core/config'
3
+ import { createResponderFactory } from '../../core/factory'
4
+
5
+ export const responderMiddleware = (
6
+ cfg?: Partial<ResponderConfig>,
7
+ ): RequestHandler => {
8
+ const factory = createResponderFactory(cfg)
9
+
10
+ return (_req, res, next) => {
11
+ ;(res as any).responder = <P = unknown>() => factory<P>(res)
12
+ next()
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "strict": true,
5
+ "esModuleInterop": true,
6
+ "skipLibCheck": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "declaration": true
9
+ },
10
+ "include": ["src/**/*"]
11
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "CommonJS",
5
+ "moduleResolution": "Node",
6
+ "rootDir": "./src",
7
+ "outDir": "./dist/cjs"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "rootDir": "./src",
7
+ "outDir": "./dist/esm"
8
+ }
9
+ }