feat: add PDF download endpoint and export createLocalizedResumeDocument function

This commit is contained in:
Amruth Pillai
2026-05-08 00:05:55 +02:00
parent 3b82aa90f3
commit 5042ad9d1f
5 changed files with 96 additions and 4 deletions
@@ -0,0 +1,13 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { Template } from "@reactive-resume/schema/templates";
import { renderToBuffer } from "@react-pdf/renderer";
import { createLocalizedResumeDocument } from "./pdf-document";
export const createResumePdfFile = async (data: ResumeData, filename: string, template?: Template) => {
const document = await createLocalizedResumeDocument(data, template);
const buffer = await renderToBuffer(document);
const bytes = new Uint8Array(new ArrayBuffer(buffer.byteLength));
bytes.set(buffer);
return new File([bytes], filename, { type: "application/pdf" });
};
+1 -1
View File
@@ -21,7 +21,7 @@ export const useLocalizedResumeDocument = (data?: ResumeData, template?: Templat
}, [data, template, sectionTitleResolver]);
};
const createLocalizedResumeDocument = async (data: ResumeData, template?: Template) => {
export const createLocalizedResumeDocument = async (data: ResumeData, template?: Template) => {
const sectionTitleResolver = await createSectionTitleResolverForLocale(data.metadata.page.locale);
return (
@@ -0,0 +1,46 @@
import { ORPCError } from "@orpc/server";
import z from "zod";
import { protectedProcedure } from "@reactive-resume/api/context";
import { resumeService } from "@reactive-resume/api/services/resume";
import { generateFilename } from "@reactive-resume/utils/file";
import { createResumePdfFile } from "@/libs/resume/pdf-document.server";
export const downloadResumePdfProcedure = protectedProcedure
.route({
method: "GET",
path: "/resumes/{id}/pdf",
tags: ["Resumes"],
operationId: "downloadResumePdf",
summary: "Download resume as PDF",
description:
"Generates a PDF for the specified resume and returns it as a forced download. Only resumes belonging to the authenticated user can be downloaded. Requires authentication.",
successDescription: "The generated resume PDF.",
outputStructure: "detailed",
})
.input(z.object({ id: z.string().describe("The ID of the resume.") }))
.output(
z.object({
headers: z.object({
"content-disposition": z.string(),
}),
body: z.file().mime("application/pdf"),
}),
)
.handler(async ({ context, input }) => {
const resume = await resumeService.getById({ id: input.id, userId: context.user.id });
const filename = generateFilename(resume.name, "pdf");
try {
const body = await createResumePdfFile(resume.data, filename);
return {
headers: {
"content-disposition": `attachment; filename="${filename}"`,
},
body,
};
} catch (error) {
console.error("[PDF API] Failed to render resume PDF", { resumeId: input.id, error });
throw new ORPCError("INTERNAL_SERVER_ERROR", { message: "Failed to generate resume PDF" });
}
});
+11 -2
View File
@@ -9,9 +9,18 @@ import router from "@reactive-resume/api/routers";
import { env } from "@reactive-resume/env/server";
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
import { getLocale } from "@/libs/locale";
import { downloadResumePdfProcedure } from "./-helpers/resume-pdf";
const openAPIRouter = {
...router,
resume: {
...router.resume,
downloadPdf: downloadResumePdfProcedure,
},
};
async function handler({ request }: { request: Request }) {
const openAPIHandler = new OpenAPIHandler(router, {
const openAPIHandler = new OpenAPIHandler(openAPIRouter, {
plugins: [
new BatchHandlerPlugin(),
new RequestHeadersPlugin(),
@@ -34,7 +43,7 @@ async function handler({ request }: { request: Request }) {
const locale = await getLocale();
if (request.method === "GET" && (request.url.endsWith("/spec.json") || request.url.endsWith("/spec"))) {
const spec = await openAPIGenerator.generate(router, {
const spec = await openAPIGenerator.generate(openAPIRouter, {
info: {
title: "Reactive Resume",
version: __APP_VERSION__,
+25 -1
View File
@@ -1730,7 +1730,8 @@
"description": "Metadata for the resume, such as template, layout, typography, etc. This section describes the overall design and appearance of the resume."
}
},
"required": ["picture", "basics", "summary", "sections", "customSections", "metadata"]
"required": ["picture", "basics", "summary", "sections", "customSections", "metadata"],
"additionalProperties": {}
}
}
},
@@ -3121,6 +3122,29 @@
}
}
},
"/resumes/{id}/pdf": {
"get": {
"operationId": "downloadResumePdf",
"summary": "Download resume as PDF",
"description": "Generates a PDF for the specified resume and returns it as a forced download. Only resumes belonging to the authenticated user can be downloaded. Requires authentication.",
"tags": ["Resumes"],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "string", "description": "The ID of the resume." }
}
],
"responses": {
"200": {
"description": "The generated resume PDF.",
"headers": { "content-disposition": { "schema": { "type": "string" }, "required": true } },
"content": { "application/pdf": { "schema": { "type": "string", "contentMediaType": "application/pdf" } } }
}
}
}
},
"/statistics/users": {
"get": {
"operationId": "getUserCount",