Last active
October 3, 2023 15:47
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
}; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice! Thanks for sharing.