Skip to content
KP
All projects
Tool·2026Live

CV Maker

Drag-and-drop CV builder. Live preview, multiple templates, PDF rendered by a background worker.

Next.js 15React 19TypeScriptTailwind v4NestJSPrismaPostgreSQLBullMQPuppeteerMinIOJWT
highlights
  • Monorepo with three apps web, REST API and a dedicated PDF worker
  • Drag-and-drop section reordering via dnd-kit with live preview
  • Background PDF generation through BullMQ queue and a Puppeteer worker
  • S3-compatible file storage via MinIO for assets and generated PDFs
  • JWT auth, rate limiting and request validation through Zod schemas
  • Shared schema and template packages reused across web, API and worker

Overview

CV Maker is a full-stack resume builder hosted under mirekdev.pl/cvmaker. Pick a template, drag sections around, type in your data, get a clean PDF. Behind the scenes it is a small distributed system: a Next.js front-end, a NestJS API and a separate worker that renders PDFs with Puppeteer.

Problem

Most CV builders fall in one of two buckets: a polished SaaS that hides the result behind a paywall, or a one-pager that exports an unstyled PDF. I wanted something in between. Free to use, decent typography, multiple templates, fast iteration without re-uploading anything, and a PDF that looks like a real document, not a screenshot of a webpage.

The technical challenge: generating high quality PDFs is slow. Doing it inline in a request makes the UI feel broken. Doing it in a Lambda costs more than the project earns.

Solution

A monorepo with three apps and two shared packages:

  • apps/web: Next.js 15 with React 19, Tailwind v4 and shadcn-style primitives. Drag-and-drop powered by dnd-kit, forms by React Hook Form with Zod validation, server state via TanStack Query, client state via Zustand.
  • apps/api: NestJS 11 with Prisma and PostgreSQL. JWT auth, Throttler for rate limiting, Helmet for headers, MinIO for object storage. Pushes PDF jobs to a BullMQ queue.
  • apps/pdf-worker: standalone NestJS process consuming the queue. Renders the chosen template through Puppeteer to PDF, uploads the result to MinIO, marks the job done.
  • packages/schema: shared Zod schemas used by both the API and the web app for end-to-end type safety.
  • packages/templates: rendered React templates that both the live preview and the worker consume.

Deployed path-based under mirekdev.pl/cvmaker/. Nginx routes:

  • /cvmaker/ -> Next.js on 3110
  • /cvmaker/api/ -> NestJS on 3111
  • /cvmaker/files/ -> MinIO proxy via the API

Tech rationale

  • Three processes instead of one: rendering PDFs needs Puppeteer, which needs a real Chromium. Keeping it in its own pm2 process means the API stays responsive and the worker can be scaled or restarted in isolation.
  • BullMQ over direct calls: jobs survive a worker crash, retry on transient errors and give the user a real "your PDF is being generated" state instead of a 30-second spinner on a stuck request.
  • MinIO over filesystem: S3-compatible API means the storage layer can move to AWS or Cloudflare R2 later without code changes.
  • Shared packages: schema and templates as workspace packages means one source of truth across web, API and worker.
  • Path-based deploy under mirekdev.pl/cvmaker/: shared nginx vhost with the portfolio, one TLS cert, three pm2 processes added to the existing pool.

Lessons learned

  • The split into web, API and worker added boilerplate up front and saved my evenings every time something broke after.
  • Puppeteer in production needs love around Chrome flags, fonts and concurrency limits. Defaults are not enough.
  • A monorepo is worth the setup the moment two apps need to share a type. Trying to keep the schemas in sync across separate repos would have been the slowest possible thing.