When each Lambda handler manages its own logging and errors, small differences become production problems. One returns a stack trace, another forgets CORS headers, and a third has no request ID when support needs to trace a failure.
I solve this with a small wrapper around every HTTP handler:
type HttpHandler = (request: RequestContext) => Promise<HttpResponse>;
export const withHttp = (run: HttpHandler) => async (event: ApiEvent) => {
const startedAt = Date.now();
const correlationId =
event.headers["x-correlation-id"] ?? crypto.randomUUID();
try {
logger.info("http.request.started", {
correlationId,
method: event.requestContext.http.method,
route: event.routeKey,
});
const response = await run(toRequestContext(event, correlationId));
return addStandardHeaders(response, correlationId);
} catch (error) {
return mapErrorToResponse(error, correlationId);
} finally {
logger.info("http.request.finished", {
correlationId,
durationMs: Date.now() - startedAt,
});
}
};
What belongs in the wrapper#
The wrapper owns policy that every request needs:
- correlation ID creation and propagation;
- structured start, finish, and failure logs;
- safe conversion from known errors to HTTP responses;
- standard security, content-type, and correlation headers;
- basic timing and cold-start context.
It may also enforce a maximum request size before parsing. Logging the byte count and a hash is often safer than recording a sensitive request body.
What does not belong there#
A global wrapper should not decide feature permissions, load domain objects, or silently retry writes. Those choices depend on the operation. Keep authentication extraction at the edge, but put authorization rules in the application where they can use the requested resource and action.
Also avoid turning middleware into an invisible pipeline of dozens of steps. A handler should remain easy to trace from request to use case.
Preserve useful failure information#
Known operational errors can expose a stable status, code, and public message. Unknown exceptions should produce a generic 500 response while the logs retain the original error and stack.
The correlation ID connects those two views: a safe message for the client and detailed evidence for the operator.
Test the wrapper once for malformed requests, known errors, unknown errors, and headers. Feature tests can then focus on feature behavior. A small shared wrapper gives every new function a dependable operational baseline from its first deployment.