aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app/api/v1/utils/handler.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/app/api/v1/utils/handler.ts')
-rw-r--r--apps/web/app/api/v1/utils/handler.ts149
1 files changed, 149 insertions, 0 deletions
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<SearchParamsT, BodyType> {
+ ctx: Context;
+ api: ReturnType<typeof createTrcpClientFromCtx>;
+ searchParams: SearchParamsT extends z.ZodTypeAny
+ ? z.infer<SearchParamsT>
+ : undefined;
+ body: BodyType extends z.ZodTypeAny
+ ? z.infer<BodyType> | undefined
+ : undefined;
+}
+
+type SchemaType<T> = T extends z.ZodTypeAny
+ ? z.infer<T> | undefined
+ : undefined;
+
+export async function buildHandler<
+ SearchParamsT extends z.ZodTypeAny | undefined,
+ BodyT extends z.ZodTypeAny | undefined,
+ InputT extends TrpcAPIRequest<SearchParamsT, BodyT>,
+>({
+ 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<SearchParamsT> | undefined = undefined;
+ if (searchParamsSchema !== undefined) {
+ searchParams = searchParamsSchema.parse(
+ Object.fromEntries(req.nextUrl.searchParams.entries()),
+ ) as SchemaType<SearchParamsT>;
+ }
+
+ let body: SchemaType<BodyT> | undefined = undefined;
+ if (bodySchema) {
+ body = bodySchema.parse(await req.json()) as SchemaType<BodyT>;
+ }
+
+ 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",
+ },
+ });
+ }
+ }
+}