ProjectDeveloper Tools · 2026
PDF Everything
A unified PDF console and REST API with nineteen tools for conversion, rendering, extraction, and document workflows.
- Next.js
- NestJS
- TypeScript
- Turborepo
- Chromium
Developer Tools · Project
PDF Everything puts nineteen document operations behind one visual console and one versioned REST API. It covers PDF manipulation, image and text work, form filling, and HTML or Markdown rendering. Anything I can do by clicking in the console, an integration can do over the same public endpoint.
Building a browser tool and an API usually means writing each operation twice, once per surface. The two copies then drift, and the console starts accepting a file the API would reject. I did not want to maintain two answers to the same question about what a valid input is.
pnpm install pnpm devconsole http://localhost:3000/pdf-everything/console/nest api http://localhost:3001/apiswagger ui http://localhost:3001/api/docspdf worker http://localhost:8020chromium http://localhost:8010 curl -sI localhost:3001/api/docs | head -1HTTP/1.1 200 OKWhere the contract lives
The types package holds Zod schemas, and both the console and the backend import them. The console is a static export with no API routes and no PDF implementation inside it. NEXT_PUBLIC_API_ORIGIN selects the Nest origin at build time, so every control in the console calls exactly the endpoint an integration would call. That is the property I was after. There is one answer to what a valid input is, and one place to change it. console/lib/tools/ keeps a module and a form per operation on top of those shared schemas, so the interface stays declarative rather than reimplementing the rules.
Gateway and workers
NestJS owns the versioned /api/v1 contract. Multipart uploads, stored file references, validation and problem responses, file persistence, routing to the right worker, and streaming the result back. It imports neither pdf-lib, nor pdfjs-dist, nor sharp, nor Chromium, so a native document dependency cannot fail inside the public API process. pdf-core-worker listens on port 8020 and accepts a private POST /v1/execute/:operation. The Chromium worker listens on 8010 and serves /v1/render/html and /v1/render/markdown. Chromium keeps its own lockfile and its own image, because its size, cold starts, memory profile and security boundary have nothing in common with pdf-lib.
Calling the API
Upload once, then address the file by id, so a second operation does not resend the bytes.
curl -X POST "$API_ORIGIN/api/v1/files" -F "[email protected]"
curl -X POST "$API_ORIGIN/api/v1/organize/rotate?output=ref" \
-H 'content-type: application/json' \
-d '{"fileIds":["FILE_ID"],"options":{"angle":90}}'
Page ranges are one-based strings such as 1-3,5,7-9. Out-of-range pages are ignored, while a reversed range or page zero is invalid. The default response is a binary download, and ?output=ref returns FileMeta JSON instead. Split returns a list of files, forms extract returns field descriptions, and text extraction returns text/plain with an X-Page-Count header. Uploaded files and fileIds cannot be mixed in one request.
Decisions and costs
- Group workers by runtime rather than by operation. Seventeen operations share one Node.js image because they share the same
pdf-lib,pdfjs-distandsharpprofile. Chromium is separate for exactly the same reason inverted. One service per operation was rejected on purpose. - Keep the console a static export. It ships to a CDN, holds no secrets, and cannot do anything the public API does not already expose. The cost is that the console cannot optimize a flow by shortcutting the gateway.
- Send file bytes to the core worker as base64 inside the internal JSON protocol. It is simple and synchronous, and it caps request size and rules out a clean Lambda deployment for core operations. That is the decision I would revisit, in favour of signed object-storage references.
Limits and security
Documents and HTML are treated as untrusted. Execution routes stay private or authenticated, worker tokens never reach the console, and Chromium runs with --no-sandbox inside its container, so platform isolation carries that weight. The Chromium worker currently allows remote images, fonts and stylesheets, which wants an egress allowlist before it accepts arbitrary multi-tenant HTML. Lambda suits small HTML and Markdown jobs today, while core operations need the object-storage protocol first. Worker images publish to ghcr.io, and production should pin a digest rather than latest. It is open source, with Swagger served next to the API and a Fumadocs site alongside the console. Before pushing I run pnpm lint && pnpm typecheck && pnpm build && pnpm test.
Next projectVoice AI