Building an OpenAPI compatible API for Cloudflare Workers in Go

Seán Murphy
14 min readMay 5, 2024

In this post, I describe how I built a simple test API in Go and deployed it to Cloudflare Workers. I really like the Cloudflare Workers platform and was interested to understand how to do this. It’s worth noting that Go is currently not officially supported by Cloudflare, although the Workers do run WASM so it is possible to run Go code on the workers by compiling it to WASM. The test API developed here supports a few simple operations relating to managing results for running events.

When starting this, it was not at all clear which parts could be assembled to make this work — I learnt that I could use the following:

  • wrangler - the Cloudflare tool for interacting with workers; wrangler supports running the code locally in dev mode and also deploying it to Cloudflare’s platform. It also supports management operations such as database migrations and directly querying a database. wrangler is based on node so you may require a functioning node installation;
  • tinygo - this was necessary to build to a small WASM compatible target which could be deployed to the Cloudflare platform;
  • ogen - a tool to generate Go code for the OpenAPI API definition;
  • sqlc - a tool to generate callable Go code from SQL database queries; it performs type validation for the database records and provides functions which make specific SQL queries;
  • syumai/workers - this is a Go project which supports deployment of Cloudflare workers and accessing functionality of the Cloudflare platform such as the database (D1) and the key-value store.

As can be seen, there are a few moving parts and how they fit together is not entirely obvious — hopefully the main points are made clear in this post.

The git repo containing the code which goes with this is here.

Running the API locally

To run the API locally, you will need to install wrangler, tinygo, ogen and sqlc. Installation of these is quite straightforward and documented in the respective repos. It is assumed some other basic tooling is installed including make, curl and jq.

There are a couple of steps required to run the API locally. First, build the workers-assets-gen tool from the syumai/workers repo.

<nixos>~/Work on ☁️  (eu-west-1) 
🕙 14:06:17 ❯ git clone git@github.com:syumai/workers.git
Cloning into 'workers'...
remote: Enumerating objects: 2360, done.
remote: Counting objects: 100% (755/755), done.
remote: Compressing objects: 100% (370/370), done.
remote: Total 2360 (delta 406), reused 547 (delta 344), pack-reused 1605
Receiving objects: 100% (2360/2360), 383.67 KiB | 1.00 MiB/s, done.
Resolving deltas: 100% (1352/1352), done.

<nixos>~/Work on ☁️ (eu-west-1)
🕙 14:06:25 ❯ cd workers

<nixos>workers 🍣 main via 🐹 v1.22.1 on ☁️ (eu-west-1)
🕙 14:06:33 ❯ go build ./cmd/workers-assets-gen

<nixos>workers 🍣 main [?] via 🐹 v1.22.1 on ☁️ (eu-west-1)
🕙 14:06:41 ❯ ./workers-assets-gen --help
Usage of ./workers-assets-gen:
-mode string
build mode: tinygo or go (default "tinygo")
-o string
output dir path: defaults to "build" (default "build")

<nixos>workers 🍣 main [?] via 🐹 v1.22.1 on ☁️ (eu-west-1)
🕙 14:06:47 ❯

Next, clone the cf-race-api repo.

<nixos>workers  🍣 main [?] via 🐹 v1.22.1 on ☁️  (eu-west-1) 
🕙 14:06:47 ❯ cd ..

<nixos>~/Work on ☁️ (eu-west-1)
🕙 14:07:59 ❯ git clone git@github.com:seanrmurphy/cf-race-api.git
Cloning into 'cf-race-api'...
remote: Enumerating objects: 39, done.
remote: Counting objects: 100% (39/39), done.
remote: Compressing objects: 100% (31/31), done.
remote: Total 39 (delta 10), reused 34 (delta 6), pack-reused 0
Receiving objects: 100% (39/39), 12.26 KiB | 12.26 MiB/s, done.
Resolving deltas: 100% (10/10), done.

<nixos>~/Work on ☁️ (eu-west-1)
🕙 14:08:03 ❯

Generate the javascript files necessary to run the worker:

<nixos>~/Work on ☁️  (eu-west-1) 
🕙 14:08:03 ❯ cd cf-race-api/

<nixos>cf-race-api 🍣 main via 🐹 v1.22.1 on ☁️ (eu-west-1)
🕙 14:09:02 ❯ ../workers/workers-assets-gen

<nixos>cf-race-api 🍣 main via 🐹 v1.22.1 on ☁️ (eu-west-1)
🕙 14:09:08 ❯ ls build
shim.mjs wasm_exec.js worker.mjs

<nixos>cf-race-api 🍣 main via 🐹 v1.22.1 on ☁️ (eu-west-1)
🕙 14:09:13 ❯

Note that a directory called build has been created which contains these files.

Ensure you’re logged in to Cloudflare using wrangler login

<nixos>cf-race-api  🍣 main via 🐹 v1.22.1 on ☁️  (eu-west-1) 
🕙 14:09:59 ❯ wrangler login
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
Attempting to login via OAuth...
Opening a link in your default browser: https://dash.cloudflare.com/oauth2/<REDACTED>
Successfully logged in.

Create a database for the application to use; in this case, we call the database cf-race-api.

<nixos>cf-race-api  🍣 main via 🐹 v1.22.1 on ☁️  (eu-west-1)  took 14s
🕙 14:10:16 ❯ wrangler d1 create cf-race-api
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
✅ Successfully created DB 'cf-race-api' in region EEUR
Created your database using D1's new storage backend. The new storage backend is not yet recommended for production workloads, but backs up your data via
point-in-time restore.

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "cf-race-api"
database_id = "<REDACTED>"

Add the toml output which specifies the database to your wrangler.toml. This associates this new database with this specific project. The wrangler tool supports a number of database operations; for example, you can also list your existing databases using wrangler d1 list.

With the database created, it’s now possible to initialize it. By default, this uses the content in the migrations directory (although this can be changed by adding a field to wranger.toml) - in the git repo provided, the file 0000_create_race_info_and_results.sql is used to specify which database tables to create with which fields.

Perform the first db migration — this will just initialize a local copy of the database (which is completely separate from the Cloudflare hosted database)

<nixos>cf-race-api  🍣 main [!] via 🐹 v1.22.1 on ☁️  (eu-west-1) 
🕙 14:15:15 ❯ wrangler d1 migrations list cf-race-api --local
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
Migrations to be applied:
┌───────────────────────────────────────┐
│ Name │
├───────────────────────────────────────┤
│ 0000_create_race_info_and_results.sql │
└───────────────────────────────────────|

<nixos>cf-race-api 🍣 main [!] via 🐹 v1.22.1 via 🤖 v18.19.1 on ☁️ (eu-west-1)
🕙 14:15:21 ❯ wrangler d1 migrations apply cf-race-api --local
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
Migrations to be applied:
┌───────────────────────────────────────┐
│ name │
├───────────────────────────────────────┤
│ 0000_create_race_info_and_results.sql │
└───────────────────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database cf-race-api (REDACTED) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
┌───────────────────────────────────────┬────────┐
│ name │ status │
├───────────────────────────────────────┼────────┤
│ 0000_create_race_info_and_results.sql │ ✅ │
└───────────────────────────────────────┴────────┘

<nixos>cf-race-api 🍣 main [!] via 🐹 v1.22.1 via 🤖 v18.19.1 on ☁️ (eu-west-1)
🕙 14:15:35 ❯

With the database configured, you can now run the application locally:

<nixos>cf-race-api  🍣 main [!] via 🐹 v1.22.1 via 🤖 v18.19.1 on ☁️  (eu-west-1) 
🕙 14:15:35 ❯ wrangler dev
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
Running custom build: make build
mkdir -p build
ogen ./swagger.yaml
INFO convenient Convenient errors are not available {"reason": "operation has no \"default\" response", "at": "swagger.yaml:20:9"}
sqlc generate
go mod tidy
tinygo build -o ./build/app.wasm -target wasm -no-debug
Your worker has access to the following bindings:
- D1 Databases:
- DB: cf-race-api (REDACTED)
[wrangler:inf] Ready on http://localhost:8787
⎔ Starting local server...
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ [b] open a browser, [d] open Devtools, [l] turn off local mode, [c] clear console, [x] to exit │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

You can then populate the database using scripts provided in the testing directory.

<nixos>cf-race-api  🍣 main [!?] via 🐹 v1.22.1 via 🤖 v18.19.1 on ☁️  (eu-west-1) 
🕙 14:19:07 ❯ cd testing

<nixos>cf-race-api/testing 🍣 main [!?] on ☁️ (eu-west-1)
🕙 14:19:10 ❯ set -x APIENDPOINT http://localhost:8787

<nixos>cf-race-api/testing 🍣 main [!?] on ☁️ (eu-west-1)
🕙 14:19:16 ❯ # if using bash, the above would be export APIENDPOINT=http://localhost:8787

<nixos>cf-race-api/testing 🍣 main [!?] on ☁️ (eu-west-1)
🕙 14:19:51 ❯ ./create-races.sh

Creating first race...
{"id":1,"location":"Dublin","created_at":"2024-05-05T14:20:01+02:00","name":"Dublin Marathon","event_date":"2024-05-01","run_types":[]}

Creating second race...
{"id":2,"location":"London","created_at":"2024-05-05T14:20:02+02:00","name":"London Marathon","event_date":"2024-05-10","run_types":[]}

Creating third race...
{"id":3,"location":"Zurich ","created_at":"2024-05-05T14:20:02+02:00","name":"Zurich Marathon","event_date":"2024-06-10","run_types":[]}⏎

<nixos>cf-race-api/testing 🍣 main [!?] on ☁️ (eu-west-1)
🕙 14:20:02 ❯ cd utils

<nixos>cf-race-api/testing/utils 🍣 main [!?] on ☁️ (eu-west-1)
🕙 14:20:17 ❯ ls -l
total 8
-rwxr-xr-x 1 sean users 285 May 5 14:08 delete-all.sh
-rwxr-xr-x 1 sean users 391 May 5 14:08 query-db.sh

<nixos>cf-race-api/testing/utils 🍣 main [!?] on ☁️ (eu-west-1)
🕙 14:20:18 ❯ ./query-db.sh

Querying basic db info...
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database cf-race-api (REDACTED) from ../../.wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
─────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
name │ sql │

─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
_cf_KV │ CREATE TABLE _cf_KV ( │
key TEXT PRIMARY KEY,
value BLOB
) WITHOUT ROWID
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
│ d1_migrations │ CREATE TABLE d1_migrations( │
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
sqlite_autoindex_d1_migratio│ null │
s_1
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
sqlite_sequence │ CREATE TABLE sqlite_sequence(name,seq) │
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
│ race_info │ CREATE TABLE race_info ( │
id INTEGER PRIMARY KEY AUTOINCREMENT,
location TEXT NOT NULL,
name TEXT NOT NULL,
event_date INTEGER NOT NULL,
run_types TEXT NOT NULL,
created_at INTEGER NOT NULL
)
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
│ race_results │ CREATE TABLE race_results ( │
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
run_type TEXT NOT NULL,
race_id INTEGER NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
FOREIGN KEY(race_id) REFERENCES race_info(id)
)
─────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────

Querying race info table...
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database cf-race-api (REDACTED) from ../../.wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
┌────┬──────────┬─────────────────┬──────────────────┬─────────────────────────────────────┬──────────────────┐
│ id │ location │ name │ event_date │ run_types │ created_at │
├────┼──────────┼─────────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ 1 │ Dublin │ Dublin Marathon │ 1714521600000000 │ ["marathon","half-marathon","10km"] │ 1714911601974000 │
├────┼──────────┼─────────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ 2 │ London │ London Marathon │ 1715299200000000 │ ["marathon"] │ 1714911602015000 │
├────┼──────────┼─────────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ 3 │ Zurich │ Zurich Marathon │ 1717977600000000 │ ["marathon","half-marathon"] │ 1714911602047000 │
└────┴──────────┴─────────────────┴──────────────────┴─────────────────────────────────────┴──────────────────┘

Querying race results table...
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database cf-race-api (REDACTED) from ../../.wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.

In the above, you can see that the races have been added via the API and they are present in the database.

<nixos>cf-race-api/testing/utils  🍣 main [!?] via 🤖 v18.19.1 on ☁️  (eu-west-1) 
🕙 14:22:02 ❯ cd ..
<nixos>cf-race-api/testing 🍣 main [!?] on ☁️ (eu-west-1)
🕙 14:22:03 ❯ ./create-results.sh

Creating first race results...

Creating second race results...

Creating third race results...

<nixos>cf-race-api/testing 🍣 main [!?] on ☁️ (eu-west-1)
🕙 14:22:07 ❯ cd utils

<nixos>cf-race-api/testing/utils 🍣 main [!?] via 🤖 v18.19.1 on ☁️ (eu-west-1)
🕙 14:23:55 ❯ ./query-db.sh

Querying basic db info...
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database cf-race-api (REDACTED) from ../../.wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
─────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
name │ sql │

─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
_cf_KV │ CREATE TABLE _cf_KV ( │
key TEXT PRIMARY KEY,
value BLOB
) WITHOUT ROWID
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
│ d1_migrations │ CREATE TABLE d1_migrations( │
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
)
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
sqlite_autoindex_d1_migratio│ null │
s_1
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
sqlite_sequence │ CREATE TABLE sqlite_sequence(name,seq) │
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
│ race_info │ CREATE TABLE race_info ( │
id INTEGER PRIMARY KEY AUTOINCREMENT,
location TEXT NOT NULL,
name TEXT NOT NULL,
event_date INTEGER NOT NULL,
run_types TEXT NOT NULL,
created_at INTEGER NOT NULL
)
─────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────
│ race_results │ CREATE TABLE race_results ( │
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
run_type TEXT NOT NULL,
race_id INTEGER NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
FOREIGN KEY(race_id) REFERENCES race_info(id)
)
─────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
──── ───────────────────────────────────────────────────────────────────────────────────────────────────────

Querying race info table...
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database cf-race-api (REDACTED) from ../../.wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
┌────┬──────────┬─────────────────┬──────────────────┬─────────────────────────────────────┬──────────────────┐
│ id │ location │ name │ event_date │ run_types │ created_at │
├────┼──────────┼─────────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ 1 │ Dublin │ Dublin Marathon │ 1714521600000000 │ ["marathon","half-marathon","10km"] │ 1714911601974000 │
├────┼──────────┼─────────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ 2 │ London │ London Marathon │ 1715299200000000 │ ["marathon"] │ 1714911602015000 │
├────┼──────────┼─────────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ 3 │ Zurich │ Zurich Marathon │ 1717977600000000 │ ["marathon","half-marathon"] │ 1714911602047000 │
└────┴──────────┴─────────────────┴──────────────────┴─────────────────────────────────────┴──────────────────┘

Querying race results table...
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
🌀 Mapping SQL input into an array of statements
🌀 Executing on local database cf-race-api (REDACTED) from ../../.wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
┌────┬────────────────┬───────────────┬─────────┬──────────────────┬──────────────────┐
│ id │ name │ run_type │ race_id │ start_time │ end_time │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 1 │ John Smith │ marathon │ 1 │ 1714554240000000 │ 1714565041000000 │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 2 │ Joanne Smith │ marathon │ 1 │ 1714554180000000 │ 1714564981000000 │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 3 │ Johannes Smith │ half marathon │ 1 │ 1714554120000000 │ 1714564921000000 │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 4 │ John Smith │ marathon │ 2 │ 1714554240000000 │ 1714565041000000 │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 5 │ Joanne Smith │ marathon │ 2 │ 1714554180000000 │ 1714564981000000 │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 6 │ Johannes Smith │ half marathon │ 2 │ 1714554120000000 │ 1714564921000000 │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 7 │ John Smith │ marathon │ 3 │ 1714554240000000 │ 1714565041000000 │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 8 │ Joanne Smith │ marathon │ 3 │ 1714554180000000 │ 1714564981000000 │
├────┼────────────────┼───────────────┼─────────┼──────────────────┼──────────────────┤
│ 9 │ Johannes Smith │ half marathon │ 3 │ 1714554120000000 │ 1714564921000000 │
└────┴────────────────┴───────────────┴─────────┴──────────────────┴──────────────────┘

<nixos>cf-race-api/testing/utils 🍣 main [!?] via 🤖 v18.19.1 on ☁️ (eu-west-1)
🕙 14:23:58 ❯

As can be seen in the above, the result records have been added to the database.

Running the API on Cloudflare

With the API working locally, running it on Cloudflare is very straightforward. As with the local variant, it’s necessary to create the database and initialize the tables. Once that is done, the application can be deployed using wrangler deploy.

The --remote flag is used to initialize the databases on Cloudflare’s platform.

<nixos>cf-race-api  🍣 main [!] via 🐹 v1.22.1 via 🤖 v18.19.1 on ☁️  (eu-west-1)  took 8m26s
🕙 14:25:24 ❯ wrangler d1 migrations list cf-race-api --remote
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
Migrations to be applied:
┌───────────────────────────────────────┐
│ Name │
├───────────────────────────────────────┤
│ 0000_create_race_info_and_results.sql │
└───────────────────────────────────────┘

<nixos>cf-race-api 🍣 main [!] via 🐹 v1.22.1 via 🤖 v18.19.1 on ☁️ (eu-west-1)
🕙 14:25:34 ❯ wrangler d1 migrations apply cf-race-api --remote
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
Migrations to be applied:
┌───────────────────────────────────────┐
│ name │
├───────────────────────────────────────┤
│ 0000_create_race_info_and_results.sql │
└───────────────────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Mapping SQL input into an array of statements
🌀 Parsing 3 statements
🌀 Executing on remote database cf-race-api (<REDACTED>):
🌀 To execute on your local development database, remove the --remote flag from your wrangler command.
🚣 Executed 3 commands in 0.5356000000000001ms
┌───────────────────────────────────────┬────────┐
│ name │ status │
├───────────────────────────────────────┼────────┤
│ 0000_create_race_info_and_results.sql │ ✅ │
└───────────────────────────────────────┴────────┘

<nixos>cf-race-api 🍣 main [!] via 🐹 v1.22.1 via 🤖 v18.19.1 on ☁️ (eu-west-1)
🕙 14:25:50 ❯

To deploy:

<nixos>cf-race-api  🍣 main [!] via 🐹 v1.22.1 via 🤖 v18.19.1 on ☁️  (eu-west-1) 
🕙 14:25:50 ❯ wrangler deploy
⛅️ wrangler 3.34.2 (update available 3.53.1)
-------------------------------------------------------
Running custom build: make build
mkdir -p build
ogen ./swagger.yaml
INFO convenient Convenient errors are not available {"reason": "operation has no \"default\" response", "at": "swagger.yaml:20:9"}
sqlc generate
go mod tidy
tinygo build -o ./build/app.wasm -target wasm -no-debug
Your worker has access to the following bindings:
- D1 Databases:
- DB: cf-race-api (REDACTED)
Total Upload: 1267.81 KiB / gzip: 425.62 KiB
Uploaded cf-race-api (3.69 sec)
Published cf-race-api (6.07 sec)
https://cf-race-api.<REDACTED>.workers.dev
Current Deployment ID: <REDACTED>

And then testing can be done as before, but the environment variable needs to be set to point at the Cloudflare’s platform rather than the local devleopment environment.

<nixos>cf-race-api/testing  🍣 main [!] on ☁️  (eu-west-1) 
🕙 14:29:29 ❯ set -x APIENDPOINT https://cf-race-api.<REDACTED>.workers.dev

<nixos>cf-race-api/testing 🍣 main [!] on ☁️ (eu-west-1)
🕙 14:30:00 ❯ ./create-races.sh

Creating first race...
{"id":1,"location":"Dublin","created_at":"2024-05-05T12:30:08Z","name":"Dublin Marathon","event_date":"2024-05-01","run_types":[]}

Creating second race...
{"id":2,"location":"London","created_at":"2024-05-05T12:30:08Z","name":"London Marathon","event_date":"2024-05-10","run_types":[]}

Creating third race...
{"id":3,"location":"Zurich ","created_at":"2024-05-05T12:30:09Z","name":"Zurich Marathon","event_date":"2024-06-10","run_types":[]}⏎
<nixos>cf-race-api/testing 🍣 main [!] on ☁️ (eu-west-1)
🕙 14:30:09 ❯ ./create-results.sh

Creating first race results...

Creating second race results...

Creating third race results...

<nixos>cf-race-api/testing 🍣 main [!] on ☁️ (eu-west-1)
🕙 14:30:28 ❯ ./query-races.sh

Querying first race...
{"id":1,"location":"Dublin","created_at":"2024-05-05T12:30:08Z","name":"Dublin Marathon","event_date":"2024-05-01","run_types":["marathon","half-marathon","10km"]}

Querying second race...
{"id":2,"location":"London","created_at":"2024-05-05T12:30:08Z","name":"London Marathon","event_date":"2024-05-10","run_types":["marathon"]}

Querying third race...
{"id":3,"location":"Zurich ","created_at":"2024-05-05T12:30:09Z","name":"Zurich Marathon","event_date":"2024-06-10","run_types":["marathon","half-marathon"]}⏎
<nixos>cf-race-api/testing 🍣 main [!] on ☁️ (eu-west-1)
🕙 14:30:36 ❯ ./query-race-results.sh

Querying first race results...
[
{
"name": "John Smith",
"run_type": "marathon",
"start_time": "2024-05-01T09:04:00Z",
"end_time": "2024-05-01T12:04:01Z"
},
{
"name": "Joanne Smith",
"run_type": "marathon",
"start_time": "2024-05-01T09:03:00Z",
"end_time": "2024-05-01T12:03:01Z"
},
{
"name": "Johannes Smith",
"run_type": "half marathon",
"start_time": "2024-05-01T09:02:00Z",
"end_time": "2024-05-01T12:02:01Z"
}
]

Querying second race results...
[
{
"name": "John Smith",
"run_type": "marathon",
"start_time": "2024-05-01T09:04:00Z",
"end_time": "2024-05-01T12:04:01Z"
},
{
"name": "Joanne Smith",
"run_type": "marathon",
"start_time": "2024-05-01T09:03:00Z",
"end_time": "2024-05-01T12:03:01Z"
},
{
"name": "Johannes Smith",
"run_type": "half marathon",
"start_time": "2024-05-01T09:02:00Z",
"end_time": "2024-05-01T12:02:01Z"
}
]

Querying third race results...
[
{
"name": "John Smith",
"run_type": "marathon",
"start_time": "2024-05-01T09:04:00Z",
"end_time": "2024-05-01T12:04:01Z"
},
{
"name": "Joanne Smith",
"run_type": "marathon",
"start_time": "2024-05-01T09:03:00Z",
"end_time": "2024-05-01T12:03:01Z"
},
{
"name": "Johannes Smith",
"run_type": "half marathon",
"start_time": "2024-05-01T09:02:00Z",
"end_time": "2024-05-01T12:02:01Z"
}
]

<nixos>cf-race-api/testing 🍣 main [!] on ☁️ (eu-west-1)
🕙 14:30:47 ❯

Comments

The Cloudflare tooling provides an excellent developer experience where running things locally is very similar to running things on their platform; creating and managing databases and key-value stores is done in exactly the same way.

The Cloudflare D1 database appears to be very sqlite compatible and the Go sqlc tooling for sqlite works well with D1. It may have been possible to somehow merge the models generated by ogen and sqlc but I did not look into this; this meant that there was a bit of code necessary to map between the types created by ogen and sqlc.

ogen OpenAPI tooling for Go provides an HTTP interface which can be easily served via Cloudflare workers - it provides a standard HTTP server interface which fits easily into the Cloudflare workers entry point.

Compile time with tinygo can take maybe a minute (depending on compute resources available) and it does not automatically recompile when changes are made; this does impact the speed of developer iteration cycles and it’s likely that using Javascript on the platform would support more rapid iterations.

tinygo does result in small executables - in this work, the resulting executable was about 1MB.

Latency on the Cloudflare workers platform was not negligible; I experienced latency of around 40ms even though the free tier only guarantees that workers with 10ms latency will not be terminated.

The restriction on executable size and latency do not apply for the paid tier; perhaps it’s not necessary to use tinygo if the paid tier is in use.

It’s also possible to specify other configuration aspects in the wrangler.toml file, including, for example, a KV store and a rate limiter.

There are still quite a few cleanups to be done to the above code, but a solution to serving an OpenAPI service via Cloudflare Workers in Go is reasonably clear.

To make modifications to this project, eg to create your own functionality, the workflow is as follows:

  • modify the OpenAPI definition in swagger.yaml;
  • run ogen to generate the new Go code for the new API
  • modify service.go to implement the new service
  • run wrangler dev for testing the system locally

Closing remarks

Bringing up a basic API on Cloudflare workers developed in Go for test purposes is reasonably straightforward. In the next post, I’ll add some basic authentication mechanisms and a Key Value store.

--

--

Seán Murphy

Tech Tinkerer, Curious Thinker(er). Lost Leprechaun. Always trying to improve.