CV Maker
Drag-and-drop CV builder. Live preview, multiple templates, PDF rendered by a background worker.
- 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.