The Node.js back-end of Snapaper
The API lists CAIE subjects, exam sessions and papers by reading PapaCambridge. Paper URLs point straight at PapaCambridge, so the API never proxies files.
Every listing it reads is kept in a last-known-good cache:
- A listing is fresh for 12 hours. After that it is still served instantly while one background request refreshes it.
- A failed refresh keeps serving the old copy and retries 10 minutes later. Errors and empty pages are never cached.
- Redis shares the cache between machines and keeps it through restarts and deploys. An in-memory copy keeps serving while Redis is down.
As a result, anything that has been viewed before keeps working while PapaCambridge is down. Only listings nobody has requested yet return 502.
| Route | Response |
|---|---|
GET /api/cates/ppca/:level |
Subjects for as-and-a-level or igcse |
GET /api/years/ppca/:level/:subject |
Exam sessions for a subject, newest first |
GET /api/papers/ppca/:level/:subject/:session |
Papers for a session |
GET /healthz |
ok, without touching Redis or PapaCambridge |
:subject only needs to end with the 4-digit syllabus code, e.g. physics-(9702).
Requires Node.js 22 or later and pnpm.
git clone git@github.com:Snapaper/snapaper-nodejs.git
pnpm install
pnpm devThe API listens on port 8080 unless PORT is set. Redis is optional: put REDIS_URL=redis://... in a .env file to use one, or run without it and the in-memory cache takes over.
The API runs on Fly.io with one machine in Singapore (sin) and one in Frankfurt (fra). Fly routes each user to the nearest healthy machine and stops sending traffic to a machine that fails its /healthz check, so either machine can go down without an outage.
fly deployDeploys roll one machine at a time and wait for the health check before moving on. The Redis cache is a Fly-managed Upstash database whose URL is stored in the REDIS_URL secret.
To restore the two-region layout, set each region separately:
fly scale count 1 --region sin
fly scale count 1 --region fra