Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CristianParadaLopez/cv-builder/llms.txt

Use this file to discover all available pages before exploring further.

The handleDownloadPDF controller function is implemented in backend/src/controllers/cv.controller.ts and uses headless Chromium via Playwright to convert a CV HTML document into a print-quality A4 PDF. However, this function is not currently registered in backend/src/routes/cv.routes.ts, which means calling POST /api/cv/pdf against the running server will return a 404. The three routes that are registered are /generate, /edit, and /suggest.
POST /api/cv/pdf is not a callable production endpoint in the current codebase. The route is absent from cv.routes.ts. The documentation below describes what the controller does and how to wire it up if you are self-hosting and want to enable PDF generation on the server.

What the controller does

handleDownloadPDF accepts a JSON body containing a html string, launches a headless Chromium browser using Playwright, loads the HTML, waits for fonts and styles to settle, generates a PDF in memory, and streams it back to the client as a binary file download. The browser is always closed in a finally block regardless of whether the PDF generation succeeds or fails. The complete implementation from cv.controller.ts:
export async function handleDownloadPDF(req: Request, res: Response) {
  let browser = null;

  try {
    const { html } = req.body;

    if (!html || typeof html !== "string") {
      return res.status(400).json({ error: "HTML es requerido" });
    }

    browser = await chromium.launch({ headless: true });
    const page = await browser.newPage();

    await page.setContent(html, {
      waitUntil: "networkidle",
      timeout: 15000,
    });

    await page.waitForTimeout(1200);

    const pdfBuffer = await page.pdf({
      format: "A4",
      printBackground: true,
      margin: { top: "0", right: "0", bottom: "0", left: "0" },
      preferCSSPageSize: true,
    });

    res.setHeader("Content-Type", "application/pdf");
    res.setHeader("Content-Disposition", 'attachment; filename="mi-cv-skillara.pdf"');
    res.setHeader("Content-Length", pdfBuffer.length);
    res.send(pdfBuffer);

  } catch (error) {
    console.error("❌ Error generando PDF:", error);
    return res.status(500).json({
      error: "Error al generar el PDF. Intentá de nuevo.",
    });
  } finally {
    if (browser) await browser.close();
  }
}

How to register the route (self-hosting)

To enable the /api/cv/pdf endpoint, import handleDownloadPDF in cv.routes.ts and add a router.post call:
// backend/src/routes/cv.routes.ts
import { Router } from "express";
import {
  handleGenerateCV,
  handleEditCV,
  handleSuggestField,
  handleDownloadPDF,          // add this import
} from "../controllers/cv.controller";

const router = Router();

router.post("/generate", handleGenerateCV);
router.post("/edit", handleEditCV);
router.post("/suggest", handleSuggestField);
router.post("/pdf", handleDownloadPDF);    // add this line

export default router;
After adding the route, restart the backend. The endpoint will be available at POST /api/cv/pdf.

Expected request (once registered)

Method: POST
Path: /api/cv/pdf
Content-Type: application/json
FieldTypeRequiredDescription
htmlstringYesThe complete HTML document to render. Must be a non-empty string. Pass the html value returned by /api/cv/generate or /api/cv/edit.

Expected response (once registered)

Status: 200 OK
HeaderValue
Content-Typeapplication/pdf
Content-Dispositionattachment; filename="mi-cv-skillara.pdf"
Content-LengthByte length of the PDF buffer
The response body is raw binary PDF data. Use --output in curl or call response.blob() in the browser to capture it.

Expected error responses (once registered)

StatusCondition
400html is missing or not a string
500Playwright failed to launch Chromium, or the page render timed out
Error responses at 500 are returned as JSON even though the success response is binary:
{ "error": "Error al generar el PDF. Intentá de nuevo." }

Example curl (once registered)

curl -X POST http://localhost:3001/api/cv/pdf \
  -H 'Content-Type: application/json' \
  -d '{"html": "<!DOCTYPE html>..."}' \
  --output my-cv.pdf

Frontend usage reference

The Skillara React frontend includes a downloadPDF function in frontend/src/pages/services/api.ts that targets this endpoint. This client-side code exists and calls POST /api/cv/pdf, but the call will fail with a 404 until the route is registered on the backend:
export async function downloadPDF(html: string): Promise<Blob> {
  const response = await fetch(`${API_URL}/api/cv/pdf`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ html }),
  });
  if (!response.ok) {
    const contentType = response.headers.get("content-type") || "";
    if (contentType.includes("application/json")) {
      const data = await response.json().catch(() => ({}));
      throw new Error(data.error || "Error al generar el PDF");
    }
    throw new Error(`Error ${response.status} al generar el PDF`);
  }
  const contentType = response.headers.get("content-type") || "";
  if (!contentType.includes("application/pdf")) {
    const text = await response.text();
    throw new Error(`Respuesta inesperada del servidor: ${text.slice(0, 100)}`);
  }
  return response.blob();
}

PDF generation behavior

Once the route is registered, the controller follows these steps:
  1. Validates input — returns 400 immediately if html is missing or not a string.
  2. Launches headless Chromium via chromium.launch({ headless: true }).
  3. Loads the HTML with page.setContent(html, { waitUntil: "networkidle", timeout: 15000 }). The networkidle condition ensures all inline resources have settled before rendering begins.
  4. Waits 1200 ms for web fonts and CSS animations to finish painting.
  5. Generates the PDF with format: "A4", printBackground: true, zero margins, and preferCSSPageSize: true to respect @page rules embedded in the HTML’s <style> block.
  6. Streams the buffer back to the client and closes the browser in the finally block.
Enabling this endpoint requires Playwright and Chromium to be installed on the server. Run npx playwright install chromium during backend setup. On Render or similar cloud platforms, add this command to your build script or use a Docker image that includes Chromium.
The networkidle wait combined with the 1200 ms font delay means each PDF request can take 3–8 seconds on cold hardware. Consider adding rate limiting before exposing this endpoint publicly.

Build docs developers (and LLMs) love