Why Migrate
Project 04 proved the API concept worked. But Railway has a cost at scale, and the Node.js runtime adds infrastructure complexity that isn’t necessary for an API that serves simple flat data.
Cloudflare Workers run on the edge — the code executes in the data centre closest to the user. D1 is Cloudflare’s SQLite database, available on the free tier with 5GB storage. The migration made the API genuinely serverless with zero monthly cost and no servers to maintain.
It was also the right moment to add TypeScript. The JavaScript version had no validation on incoming request bodies — a number field could receive a string and the database would only complain at query time. TypeScript plus Zod closes that gap entirely.
What Changed
| v1 (Node + Railway) | v2 (Workers + D1) |
|---|---|
| Express | Cloudflare Worker fetch handler |
PostgreSQL via pg | D1 (SQLite at the edge) |
| JavaScript | TypeScript |
| No validation library | Zod schema validation |
railway.toml | wrangler.toml |
npm start | wrangler deploy |
TypeScript — What It Adds
Every moving part has a named type. The D1 database row, the API response
shape, the Cloudflare environment bindings — all defined once in
src/types/index.ts and referenced everywhere else.
interface FuelPriceRow {
id: number;
month: string;
month_label: string;
p95i: number;
p95c: number;
p93i: number;
d500i: number;
d500c: number;
d50i: number;
d50c: number;
source: string;
updated_at: string;
}
TypeScript catches the mismatch between what D1 returns and what the API sends before the code ever runs — not at runtime in production.
Zod Validation
Every write endpoint validates its request body with a Zod schema before touching the database:
const PostBody = z.object({
month: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/),
monthLabel: z.string().min(1),
p95i: z.number().positive(),
p95c: z.number().positive(),
p93i: z.number().positive(),
d500i: z.number().positive(),
d500c: z.number().positive(),
d50i: z.number().positive(),
d50c: z.number().positive(),
});
A missing field or a string where a number is expected returns a clear
400 VALIDATION_ERROR with the exact problem — not a cryptic database error.
D1 vs PostgreSQL
D1 is SQLite. The schema changes are minimal — SERIAL becomes
INTEGER PRIMARY KEY AUTOINCREMENT, DECIMAL(5,2) becomes REAL,
TIMESTAMPTZ becomes TEXT. SQLite triggers work identically for
the updated_at auto-maintenance. The COALESCE partial update pattern
carries over unchanged.
The pg parameterised query syntax ($1, $2) changes to D1’s
positional binding (?1, ?2). The pattern is the same — user values
are never concatenated into SQL strings.
Diesel Grade Naming Fix
The original API used d005i and d005c — ambiguous between 0.05% (500ppm)
and 0.005% (50ppm) sulphur. The DMRE actually publishes both grades.
v2 uses unambiguous ppm notation:
d500i/d500c— Diesel 500ppm (0.05% sulphur) — standard graded50i/d50c— Diesel 50ppm (0.005% sulphur) — low sulphur grade
Both grades are now in the database and returned in every response.
Static Assets
Cloudflare Workers don’t serve static files automatically. The docs page
required adding an ASSETS binding in wrangler.toml and updating the
Env interface:
interface Env {
DB: D1Database;
API_KEY: string;
ASSETS: Fetcher; // serves public/ directory
}
The /docs route then uses env.ASSETS.fetch() instead of a plain
fetch() — Cloudflare handles the file lookup, caching and delivery.
Windows Encoding Gotcha
Generating the seed SQL file using PowerShell redirect (>) produces
UTF-16 LE with a BOM. D1 rejects this. The fix was to write the file
directly from Node.js using fs.writeFileSync(path, content, 'utf8') —
no shell redirect involved.
What This Project Demonstrates
- Cloudflare Workers — fetch handler, URL routing, static asset serving
- D1 — SQLite at the edge, typed queries, triggers, COALESCE updates
- TypeScript strict mode — typed D1 results, Zod validation, utility types
- Wrangler — local dev, D1 migrations, secret management, deploy pipeline
- Zero infrastructure cost — no server, no database bill, no Railway
Live API: sa-fuel-api.guerillagardeningkzn.workers.dev Docs: /docs GitHub: sa-fuel-api-v2
Next: Project 06 — S Sculpt. A full-stack booking and business management platform for a live salon business, with Postgres Row-Level Security standing in for a backend server entirely.