← Back to Blog

Project 05 — TypeScript + Cloudflare Workers + D1

Migrating the SA Fuel Price API from Node.js + Express + PostgreSQL on Railway to TypeScript + Cloudflare Workers + D1. Zero infrastructure cost, full type safety, and a correction to the diesel grade naming.

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)
ExpressCloudflare Worker fetch handler
PostgreSQL via pgD1 (SQLite at the edge)
JavaScriptTypeScript
No validation libraryZod schema validation
railway.tomlwrangler.toml
npm startwrangler 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 grade
  • d50i / 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.