PROJECT — OPEN SOURCE · LIVE
ATS CV Maker
A free CV builder that exports a real .docx or PDF an applicant tracking system can read — no signup, no database, no stored CVs.
Role: Solo developer — API, front end, test suites, deployment · Status: Live and open source under Apache-2.0
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/templatesandGET /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.tomlandrender.yamlkeep 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-Fortrusted only when explicitly enabled. - Production fail-fast — the backend refuses to start unless
CORS_ORIGINSnames 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.