Skip to content

Commit 11b9804

Browse files
committed
feat: add middleware utils
1 parent 4777bef commit 11b9804

2 files changed

Lines changed: 438 additions & 0 deletions

File tree

src/utils/middleware.utils.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { randomUUID } from "crypto";
2+
import { Config } from "../config";
3+
import { uuid } from "./id.utils";
4+
import { HttpStatusCodes } from "./http-status-codes";
5+
import { ErrorResponse } from "./response.utils";
6+
import { Env } from "./env.utils";
7+
import { getLogger } from "./logger.utils";
8+
9+
/**
10+
* Type definitions for Express-compatible middleware
11+
*/
12+
export type Request = {
13+
headers: Record<string, string | string[] | undefined>;
14+
method: string;
15+
url: string;
16+
ip?: string;
17+
body?: any;
18+
query?: Record<string, any>;
19+
params?: Record<string, any>;
20+
[key: string]: any;
21+
};
22+
23+
export type Response = {
24+
status: (code: number) => Response;
25+
json: (data: any) => void;
26+
send: (data: any) => void;
27+
setHeader: (name: string, value: string | string[]) => void;
28+
end: (data?: any) => void;
29+
on: (event: string, callback: (...args: any[]) => void) => void;
30+
[key: string]: any;
31+
};
32+
33+
export type NextFunction = (err?: Error | any) => void;
34+
35+
export type Middleware = (
36+
req: Request,
37+
res: Response,
38+
next: NextFunction,
39+
) => void | Promise<void>;
40+
41+
/**
42+
* Attaches a unique request ID to each request.
43+
* Useful for request tracing and correlation between logs.
44+
*
45+
* @param {object} [options] - Configuration options
46+
* @param {string} [options.headerName='X-Request-ID'] - Header name for request ID
47+
* @param {boolean} [options.exposeHeader=true] - Whether to expose the header in response
48+
* @returns {Middleware} Express-compatible middleware
49+
*/
50+
export function requestId(options?: {
51+
headerName?: string;
52+
exposeHeader?: boolean;
53+
}): Middleware {
54+
const headerName = options?.headerName || "X-Request-ID";
55+
const exposeHeader = options?.exposeHeader !== false;
56+
57+
return (req, res, next) => {
58+
// Use existing request ID from header or generate a new one
59+
const existingId = req.headers[headerName.toLowerCase()];
60+
const id = (existingId as string) || randomUUID();
61+
62+
// Attach ID to request object
63+
req.id = id;
64+
65+
// Add ID to response headers
66+
if (exposeHeader) {
67+
res.setHeader(headerName, id);
68+
}
69+
70+
next();
71+
};
72+
}
73+
74+
/**
75+
* Measures request processing time and logs or adds it to response headers.
76+
*
77+
* @param {object} [options] - Configuration options
78+
* @param {boolean} [options.addHeader=true] - Whether to add X-Response-Time header
79+
* @param {boolean} [options.logOnComplete=false] - Whether to log timing info
80+
* @returns {Middleware} Express-compatible middleware
81+
*/
82+
export function responseTime(options?: {
83+
addHeader?: boolean;
84+
logOnComplete?: boolean;
85+
}): Middleware {
86+
const addHeader = options?.addHeader !== false;
87+
const logOnComplete = options?.logOnComplete === true;
88+
89+
return (req, res, next) => {
90+
const start = process.hrtime();
91+
92+
// Function to calculate elapsed time
93+
const calculateDuration = (): number => {
94+
const diff = process.hrtime(start);
95+
return diff[0] * 1e3 + diff[1] * 1e-6; // Convert to ms
96+
};
97+
98+
// Handle response completion
99+
res.on("finish", () => {
100+
const duration = calculateDuration();
101+
102+
if (addHeader) {
103+
res.setHeader("X-Response-Time", `${duration.toFixed(2)}ms`);
104+
}
105+
106+
if (logOnComplete) {
107+
const { method, url } = req;
108+
getLogger().info(`${method} ${url} - ${duration.toFixed(2)}ms`);
109+
}
110+
});
111+
112+
next();
113+
};
114+
}
115+
116+
/**
117+
* Request timeout middleware.
118+
* Aborts requests that take too long to process.
119+
*
120+
* @param {number} [timeoutMs=30000] - Timeout in milliseconds
121+
* @returns {Middleware} Express-compatible middleware
122+
*/
123+
export function timeout(timeoutMs: number = Config.Http.timeout): Middleware {
124+
return (req, res, next) => {
125+
// Set timeout for the request
126+
const timer = setTimeout(() => {
127+
res.status(408).json({
128+
error: true,
129+
message: "Request timeout",
130+
status: 408,
131+
timestamp: new Date().toISOString(),
132+
});
133+
}, timeoutMs);
134+
135+
// Clear timeout when response is sent
136+
res.on("finish", () => {
137+
clearTimeout(timer);
138+
});
139+
140+
next();
141+
};
142+
}
143+
144+
/**
145+
* Creates a standardized error response object for API errors.
146+
*
147+
* @param {string} message - Error message
148+
* @param {Request} req - Express request object
149+
* @param {any} error - Original error object
150+
* @param {object} [options] - Additional options
151+
* @param {boolean} [options.includeDetails=false] - Whether to include error details in non-production
152+
* @returns {object} Formatted error response
153+
*/
154+
const createErrorResponse = (
155+
message: string,
156+
req: Request,
157+
error: any,
158+
options?: { includeDetails?: boolean },
159+
) => {
160+
const isDev = Env.isDev();
161+
const includeDetails = options?.includeDetails && isDev;
162+
163+
const response: Record<string, any> = {
164+
error: true,
165+
message,
166+
timestamp: new Date().toISOString(),
167+
requestId: error?.requestId || req.id || uuid(),
168+
path: req.originalUrl || req.url,
169+
};
170+
171+
// Include error code if present
172+
if (error?.code) {
173+
response.code = error.code;
174+
}
175+
176+
// Include stack trace in development mode if requested
177+
if (includeDetails && error?.stack) {
178+
response.stack = error.stack.split("\n").map((line: string) => line.trim());
179+
}
180+
181+
return response;
182+
};
183+
184+
/**
185+
* Global error handling middleware with enhanced features.
186+
*
187+
* @param {object} [options] - Error handler options
188+
* @param {boolean} [options.logErrors=true] - Whether to log errors
189+
* @param {boolean} [options.includeDetails=false] - Whether to include error details in non-production
190+
* @param {Function} [options.logger=getLogger().error] - Custom logging function
191+
* @returns {(err: any, req: Request, res: Response, next: NextFunction) => void} Error middleware
192+
*/
193+
export function errorHandler(options?: {
194+
logErrors?: boolean;
195+
includeDetails?: boolean;
196+
logger?: (message: string, error: any) => void;
197+
}) {
198+
const logErrors = options?.logErrors !== false;
199+
const includeDetails = options?.includeDetails === true;
200+
const logger = options?.logger || getLogger().error;
201+
202+
return (err: any, req: Request, res: Response, _next: NextFunction) => {
203+
// Determine status code
204+
const status =
205+
err?.status || err?.statusCode || HttpStatusCodes.INTERNAL_SERVER_ERROR;
206+
207+
// Log error if enabled
208+
if (logErrors) {
209+
const logMessage = `[ERROR] ${req.method} ${req.originalUrl || req.url}: ${err.message || "Unknown error"}`;
210+
logger(logMessage, err);
211+
}
212+
213+
// Handle ErrorResponse instances (our custom error class)
214+
if (err instanceof ErrorResponse) {
215+
return res.status(status).json({
216+
error: err.error,
217+
message: err.message,
218+
timestamp: err.timestamp,
219+
requestId: err.requestId || req.id || uuid(),
220+
path: req.originalUrl || req.url,
221+
});
222+
}
223+
224+
// Handle any other errors
225+
return res.status(status).json(
226+
createErrorResponse(err?.message || "Internal Server Error", req, err, {
227+
includeDetails,
228+
}),
229+
);
230+
};
231+
}

0 commit comments

Comments
 (0)