Skip to content

Instantly share code, notes, and snippets.

@nick-cheatwood7
Last active October 3, 2023 15:47
Show Gist options
  • Save nick-cheatwood7/68a127ce79d04fee35bedeb44157b4d3 to your computer and use it in GitHub Desktop.
Save nick-cheatwood7/68a127ce79d04fee35bedeb44157b4d3 to your computer and use it in GitHub Desktop.
Proof-of-concept using custom middleware with Next.js 13 route handlers.
// src/lib/middleware.ts
import { randomUUID as uuid } from "crypto";
import chalk from "chalk";
import { NextFetchEvent, NextRequest, NextResponse } from "next/server";
import { ZodError } from "zod";
export type MaybePromise<T> = T | Promise<T>;
export type FetchRequestHandler = (
req: Request | NextRequest,
event?: NextFetchEvent
) => MaybePromise<Response>;
function logRequest(req: Request, elapsed: number) {
switch (req.method) {
case "GET":
console.log(
chalk.bgGreen(`[${req.method}]`),
`${req.url} (took ${elapsed}ms)`
);
break;
default:
console.log(
chalk.bgCyan(`[${req.method}]`),
`${req.url} (took ${elapsed}ms)`
);
break;
}
}
export function withLogger(next: FetchRequestHandler): FetchRequestHandler {
return async (req, event) => {
const start = Date.now();
const result = await next(req, event);
const elapsed = Date.now() - start;
result.headers.set("x-response-time", elapsed.toFixed());
if (req) {
logRequest(req, elapsed);
}
return result;
};
}
export function attachRequestId(
next: FetchRequestHandler
): FetchRequestHandler {
return async (req, event) => {
const res = await next(req, event);
res.headers.set("x-request-id", uuid());
return res;
};
}
export function withUser(next: FetchRequestHandler): FetchRequestHandler {
return (req, event) => {
const isAuth =
req.headers.get("authorization")?.replace("Bearer ", "") === "letmein";
if (!isAuth) {
console.log("Not authorized!");
return NextResponse.json(
{ error: "Please log back in." },
{ status: 401 }
);
}
return next(req, event);
};
}
export function globalErrorHandler(
next: FetchRequestHandler
): FetchRequestHandler {
return async (req, event) => {
try {
const res = await next(req, event);
return res;
} catch (error) {
console.error(error);
if (error instanceof ZodError) {
return NextResponse.json(
{
code: error.errors[0].code,
path: error.errors[0].path,
error: error.errors[0].message,
},
{ status: 400 }
);
}
if (
error instanceof SyntaxError &&
error.message === "Unexpected end of JSON input"
) {
return NextResponse.json({ error: "Invalid Payload" }, { status: 400 });
}
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
};
}
type MiddlewareFactory = (
middleware: FetchRequestHandler
) => FetchRequestHandler;
export function withMiddleware(
functions: MiddlewareFactory[] = [],
handler: FetchRequestHandler,
index = 0
): FetchRequestHandler {
const current = functions[index];
if (current) {
const next = withMiddleware(functions, handler, index + 1);
return current(next);
}
return handler;
}
/**
* GLOBAL MIDDLEWARE
* Middleware that should be run globally, regardless of what route
*/
export function withGlobalMiddleware(
next: FetchRequestHandler
): FetchRequestHandler {
return (req, event) => {
const middleware: MiddlewareFactory[] = [
withLogger,
attachRequestId,
globalErrorHandler,
];
return withMiddleware([...middleware], next)(req, event);
};
}
// src/app/api/health/route.ts
import { withGlobalMiddleware, withMiddleware } from "src/lib/middleware";
const handler = () => {
return new Response("OK");
};
export const GET = withGlobalMiddleware(handler);
// or
export const GET = withMiddleware([...], handler);
// or
export const GET = withGlobalMiddleware(withMiddleware([...]), handler);
@wontondon
Copy link

Nice! Thanks for sharing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment