PROJECT — OPEN SOURCE · LIVE

ATS CV Maker

ATS CV Maker — project title card: free ATS CV generator, no signup, Word or PDF
Fig: ATS CV Maker — a live, open-source CV builder

A free CV builder that exports a real .docx or PDF an applicant tracking system can read — no signup, no database, no stored CVs.

FastAPIReact 18TypeScriptpython-docxReportLabpypdf

Role: Solo developer — API, front end, test suites, deployment · Status: Live and open source under Apache-2.0

Open the live app Read the source

The problem

Applicant tracking systems read the file before a person does. They extract text and infer structure, so a CV that looks correct on screen can lose its section headings, its date ranges or its bullet structure the moment it is parsed — and the candidate never learns why the application went quiet.

The tools that solve this usually solve a different problem too: they ask for an account, keep the document, and charge at export. I wanted the opposite — a form a first-time job seeker can finish in a few minutes, that produces a file which survives parsing, at no cost, without retaining anyone's CV.

Constraints set before the first line of code

  • No accounts, no database. If there is nowhere to store a CV, there is nothing to breach. Generation has to be a request, not a saved record.
  • One layout definition, two output formats. The DOCX and the PDF must never drift apart, because that is how a document starts failing parsers after a template tweak.
  • Free-tier deployment. Netlify for the front end, Render for the API — cold starts included, not designed around.
  • Real parsing, not regex theatre. Import and scoring had to work on the text an ATS would actually extract.

Approach

The API is a pure function. POST /api/generate-cv takes validated JSON and returns file bytes. Nothing is written to disk, so a request is either answered or dropped — there is no partial state to clean up and no retention window to explain.

The layout is data, not markup. A single layout module describes the CV as an ordered sequence of blocks. python-docx renders that sequence into OOXML; ReportLab renders the same sequence into PDF. Adding a template or a section is a change to one description instead of two renderers kept in sync by hand.

Export the real format. The DOCX is a genuine OOXML document that Word opens natively, not HTML wearing a .doc extension — one of the quickest ways to fail a parser. PDF extraction uses pypdf; PyMuPDF sat in the first build and left the production path once pypdf covered the same job, removing a heavy native dependency.

Uploads are read in memory and discarded. Import and scoring parse the file, return JSON, and drop the bytes. Drafts never leave the browser: they autosave to local storage, debounced, and are offered back after a refresh.

Architecture

  • Front end — React 18, TypeScript, Vite. A structured editor with a live A4 preview that mirrors the generated document, one input per achievement instead of a single textarea, and an Editor/Preview tab switch on small screens.
  • Back end — FastAPI with Pydantic v2 validation, stateless, single worker. Five endpoints: POST /api/generate-cv, POST /api/score-cv, POST /api/import-cv, GET /api/templates and GET /api/health. OpenAPI 3.1.0 is served at /openapi.json.
  • Documents — python-docx for DOCX, ReportLab for PDF, pypdf for text extraction, sharing one layout model.
  • Delivery — Netlify builds the front end, Render runs uvicorn behind a health check, and netlify.toml and render.yaml keep both configurations in the repository rather than in a dashboard.
  • Profiles — an experienced and a fresher ordering. The fresher profile puts education and projects first, renames the summary to a career objective and treats work experience as optional.

Three templates, one document model

Classic, Compact and Modern differ in type scale, margins and header alignment — not in content order or section rules. That separation is deliberate: a template should change how the document reads, never which facts it contains. The same model also produces the on-screen preview, so what a user sees while typing is the document they download.

How the score works

The checker scores an uploaded CV out of 100 across six weighted categories: keywords 25, formatting 20, experience 20, contact details 15, length 10 and headings 10. Every failed check returns a severity, an explanation and a suggested fix, sorted so the highest-impact problems appear first. Grades map to A ≥ 85, B ≥ 70, C ≥ 55, D ≥ 40, otherwise F.

This is a readiness heuristic, not a reproduction of any commercial ATS algorithm — no vendor publishes a scoring specification. The score is the headline; the ordered fix list is the useful part. An optional job-description comparison reports keyword overlap alongside the readiness total without changing it, so a good CV does not score badly for targeting the wrong advert.

Hardening an unauthenticated API

Endpoints that anyone can call, that parse files, on a free tier, need limits before they need features:

  • 1 MB cap on JSON request bodies, rejected with 413.
  • 5 MB cap on uploads, with 200,000 characters of extracted text at most.
  • Archive guards — a DOCX is a ZIP, so entry count (5,000), uncompressed size (50 MB) and compression ratio (200:1) are inspected before any parser touches the bytes.
  • 30 requests per minute per IP on the CPU-heavy endpoints, with X-Forwarded-For trusted only when explicitly enabled.
  • Production fail-fast — the backend refuses to start unless CORS_ORIGINS names the deployed origin, instead of failing later as a browser error.

Testing and CI

Nine backend test modules cover generation, PDF output, import, scoring, templates, profiles, rate limiting and upload safety — with a frontend suite on Vitest and Testing Library alongside. Both run in GitHub Actions on every push and pull request, so a change to the layout model cannot silently break one of the two export paths. The repository currently documents 126 backend and 71 frontend tests.

Trade-offs, and what I would change

  • Cold starts. A free Render instance sleeps after idle, so the first request can take 30–60 seconds and health checks briefly fail. Documented in the README with a cron-ping mitigation; the real fix is a paid always-on instance.
  • Rate limiting is in-process. That is correct for one worker and wrong for many — a shared store is the fix if the service scales out.
  • Import is lossy by nature. Recovered fields come back with warnings rather than silent guesses, because a confidently wrong CV is worse than an incomplete one.
  • No accounts means no saved CVs. Deliberate — there is no stored copy to promise to protect, so the privacy statement stays short and true.
  • The front end is a client-rendered shell. The HTML response carries almost no body text, which limits what a crawler can index. Prerendering that shell is the next change I would make.

Outcome

The app is live, the repository is public under Apache-2.0, the API is documented through OpenAPI with five working endpoints, and every change runs through CI. It also exercises a set of problems that come up in client work — two binary document formats from one model, defensive limits on an unauthenticated upload path, and honest failure messages when a parse cannot be trusted.

It is the clearest example I have of a small product taken end to end: schema design, rendering, hardening, tests and deployment, rather than a description of one.

Try it with a real CV — no signup, no upload kept, no paywall at export.

Open the live appView on GitHubDiscuss a build like this