From fb297eaadee9b741ddb6731a91eb4648020dcb3b Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 20 Oct 2024 13:54:05 +0000 Subject: featue: Add infra for REST APIs and implement GET /bookmarks --- apps/web/app/api/v1/utils/handler.ts | 149 +++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 apps/web/app/api/v1/utils/handler.ts (limited to 'apps/web/app/api/v1/utils/handler.ts') diff --git a/apps/web/app/api/v1/utils/handler.ts b/apps/web/app/api/v1/utils/handler.ts new file mode 100644 index 00000000..d66bb299 --- /dev/null +++ b/apps/web/app/api/v1/utils/handler.ts @@ -0,0 +1,149 @@ +import { NextRequest } from "next/server"; +import { + createContextFromRequest, + createTrcpClientFromCtx, +} from "@/server/api/client"; +import { TRPCError } from "@trpc/server"; +import { z, ZodError } from "zod"; + +import { Context } from "@hoarder/trpc"; + +function trpcCodeToHttpCode(code: TRPCError["code"]) { + switch (code) { + case "BAD_REQUEST": + case "PARSE_ERROR": + return 400; + case "UNAUTHORIZED": + return 401; + case "FORBIDDEN": + return 403; + case "NOT_FOUND": + return 404; + case "METHOD_NOT_SUPPORTED": + return 405; + case "TIMEOUT": + return 408; + case "PAYLOAD_TOO_LARGE": + return 413; + case "INTERNAL_SERVER_ERROR": + return 500; + default: + return 500; + } +} + +interface ErrorMessage { + path: (string | number)[]; + message: string; +} + +function formatZodError(error: ZodError): string { + if (!error.issues) { + return error.message || "An unknown error occurred"; + } + + const errors: ErrorMessage[] = error.issues.map((issue) => ({ + path: issue.path, + message: issue.message, + })); + + const formattedErrors = errors.map((err) => { + const path = err.path.join("."); + return path ? `${path}: ${err.message}` : err.message; + }); + + return `${formattedErrors.join(", ")}`; +} + +export interface TrpcAPIRequest { + ctx: Context; + api: ReturnType; + searchParams: SearchParamsT extends z.ZodTypeAny + ? z.infer + : undefined; + body: BodyType extends z.ZodTypeAny + ? z.infer | undefined + : undefined; +} + +type SchemaType = T extends z.ZodTypeAny + ? z.infer | undefined + : undefined; + +export async function buildHandler< + SearchParamsT extends z.ZodTypeAny | undefined, + BodyT extends z.ZodTypeAny | undefined, + InputT extends TrpcAPIRequest, +>({ + req, + handler, + searchParamsSchema, + bodySchema, +}: { + req: NextRequest; + handler: (req: InputT) => Promise<{ status: number; resp: object }>; + searchParamsSchema?: SearchParamsT | undefined; + bodySchema?: BodyT | undefined; +}) { + try { + const ctx = await createContextFromRequest(req); + const api = createTrcpClientFromCtx(ctx); + + let searchParams: SchemaType | undefined = undefined; + if (searchParamsSchema !== undefined) { + searchParams = searchParamsSchema.parse( + Object.fromEntries(req.nextUrl.searchParams.entries()), + ) as SchemaType; + } + + let body: SchemaType | undefined = undefined; + if (bodySchema) { + body = bodySchema.parse(await req.json()) as SchemaType; + } + + const { status, resp } = await handler({ + ctx, + api, + searchParams, + body, + } as InputT); + + return new Response(JSON.stringify(resp), { + status, + headers: { + "Content-Type": "application/json", + }, + }); + } catch (e) { + if (e instanceof ZodError) { + return new Response( + JSON.stringify({ code: "ParseError", message: formatZodError(e) }), + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } + if (e instanceof TRPCError) { + let message = e.message; + if (e.cause instanceof ZodError) { + message = formatZodError(e.cause); + } + return new Response(JSON.stringify({ code: e.code, error: message }), { + status: trpcCodeToHttpCode(e.code), + headers: { + "Content-Type": "application/json", + }, + }); + } else { + return new Response(JSON.stringify({ code: "UnknownError" }), { + status: 500, + headers: { + "Content-Type": "application/json", + }, + }); + } + } +} -- cgit v1.2.3-70-g09d2