A small Go service that scrapes the Cambridge UK English dictionary and exposes it as an API key-protected HTTP API and a CLI.
  • Go 98.7%
  • Dockerfile 1.3%
Find a file
2026-06-08 14:13:54 +01:00
cmd/beda Fix failures 2026-06-08 13:24:25 +01:00
internal Add inflections 2026-06-08 14:13:54 +01:00
vendor Fix build 2026-06-08 12:56:47 +01:00
.dockerignore Add inflections 2026-06-08 14:13:54 +01:00
.gitignore Initial commit 2026-06-08 12:44:36 +01:00
docker-compose.yml Fix build 2026-06-08 13:01:02 +01:00
Dockerfile Fix build 2026-06-08 13:08:49 +01:00
go.mod Fix failures 2026-06-08 13:24:25 +01:00
go.sum Initial commit 2026-06-08 12:44:36 +01:00
README.md Add inflections 2026-06-08 14:13:54 +01:00

British English Dictionary API (beda)

A small Go service that scrapes the Cambridge UK English dictionary and exposes it as an API key-protected HTTP API and a CLI.

Public API base URL: https://beda.quinlan.cloud

It can:

  • List all words in the dictionary (no definitions) by syncing Cambridge's /browse/english/ index into a local SQLite word list, then expanding each headword into its inflected forms (e.g. cookcooks, cooked, cooking) with a bundled offline database.
  • Look up a word and return the headword, parts of speech, IPA phonetic spelling, a link to the UK audio recording, and every definition with example sentences.
  • Manage API keys (create, list, get, revoke) from the CLI. All HTTP endpoints (except health) require a key.

Everything is UK / British English only.

Architecture

A single Go binary (beda) provides both the HTTP server and the CLI, sharing one SQLite database.

Component Path
Entry point cmd/beda/main.go
Scraper (lookup + browse/sync) internal/scraper
Inflection expander (offline) internal/inflect
SQLite store (keys, words, cache) internal/store
HTTP API (router, auth, handlers) internal/api
CLI commands internal/cli

Dependencies: chi (router), goquery (HTML parsing), cobra (CLI), modernc.org/sqlite (pure-Go SQLite, no cgo).

Quick start with Docker Compose

# Build and start the API (listens on http://localhost:8080)
docker compose up -d --build

# Create an API key (the secret is printed once - save it)
docker compose exec api beda keys create --label dev

# Populate the word list. A full sync is large/slow; start with one letter.
# Each headword is also expanded into its inflected forms (cook -> cooks, ...):
docker compose exec api beda words sync --letters a

# Look up a word over HTTP (replace YOUR_KEY)
curl -H "Authorization: Bearer YOUR_KEY" https://beda.quinlan.cloud/v1/lookup/cook

The SQLite database is persisted in ./data/beda.db on the host (bind-mounted to /data in the container).

The container runs as root so it can always write to the bind-mounted ./data directory. To run as a non-root user instead, add user: "<uid>:<gid>" to the api service in docker-compose.yml and make sure ./data is owned by that uid/gid on the host (e.g. chown -R <uid>:<gid> data).

Running locally (without Docker)

Requires Go 1.26+.

go build -o beda ./cmd/beda

# Direct CLI lookup (no API key or DB needed)
./beda lookup serendipity

# Create a key and run the server (defaults: db ./data/beda.db, port 8080)
./beda keys create --label local
./beda serve

The database path is configurable with --db or the BEDA_DB env var; the port with --addr or PORT.

HTTP API

Base URL: https://beda.quinlan.cloud

All /v1 endpoints require an API key, supplied as either header:

  • Authorization: Bearer <key>
  • X-API-Key: <key>
Method Path Description
GET /healthz Health check (no auth).
GET /v1/words?prefix=&length=&alpha=&limit=&offset=&all= List of words. limit defaults to 100 (max 1000). Use all=true (or limit=0) to return the entire list with no pagination. length filters to words of an exact character length. alpha=true returns only words made up entirely of the letters a-z.
GET /v1/words/count?prefix=&length=&alpha= Number of stored words matching the filter.
GET /v1/lookup/{word} Full dictionary entry for a word.

Example: list words

# A page of words
curl -H "X-API-Key: YOUR_KEY" "https://beda.quinlan.cloud/v1/words?prefix=ab&limit=5"

# The entire word list (no pagination)
curl -H "X-API-Key: YOUR_KEY" "https://beda.quinlan.cloud/v1/words?all=true"

# All five-letter words
curl -H "X-API-Key: YOUR_KEY" "https://beda.quinlan.cloud/v1/words?length=5&all=true"

# All five-letter words containing only a-z (excludes e.g. "x-ray")
curl -H "X-API-Key: YOUR_KEY" "https://beda.quinlan.cloud/v1/words?length=5&alpha=true&all=true"

Note: length counts every character in the stored word, so hyphenated multi-word entries (e.g. x-ray) include the hyphen in their length. Add alpha=true to exclude any word containing hyphens, digits, spaces or other special characters.

{
  "words": ["aardvark", "aback", "abacus", "abaft", "abalone"],
  "count": 5,
  "total": 12345,
  "limit": 5,
  "offset": 0,
  "prefix": "ab"
}

Example: look up a word

curl -H "X-API-Key: YOUR_KEY" https://beda.quinlan.cloud/v1/lookup/cook
{
  "word": "cook",
  "pos": ["verb", "noun"],
  "pronunciations": [
    {
      "lang": "uk",
      "ipa": "/kʊk/",
      "audio": "https://dictionary.cambridge.org/uk/media/english/uk_pron/u/ukc/ukcon/ukconve028.mp3"
    }
  ],
  "definitions": [
    {
      "pos": "verb",
      "text": "When you cook food, you prepare it to be eaten ...",
      "examples": ["I don't cook meat very often."]
    }
  ],
  "source_url": "https://dictionary.cambridge.org/uk/dictionary/english/cook"
}

Lookups are cached in SQLite for 30 minutes by default (beda serve --cache-ttl). Responses include an X-Cache: HIT|MISS header. A missing word returns 404.

CLI reference

beda lookup <word> [--json]          Look up a word directly via the scraper.

beda words sync [--letters a,b]      Scrape the browse index into the word list.
               [--workers N]         Concurrent fetchers (default 2).
               [--delay 500ms]       Politeness delay per worker.
               [--inflect]           Add inflected forms after syncing (default true).
beda words inflect [--prefix]        Add inflected forms of stored words offline.
beda words list [--prefix] [--length] [--alpha] [--limit] [--offset]
beda words count [--prefix] [--length] [--alpha]

beda keys create [--label <l>]       Create a key (secret shown once).
beda keys list                       List all keys.
beda keys get <id>                   Show one key.
beda keys revoke <id>                Revoke a key.

beda serve [--addr :8080] [--cache-ttl 30m]   Run the HTTP API.

Global flag: --db <path> (or BEDA_DB) selects the SQLite database.

Notes

  • The full Cambridge word list is very large, so words sync is resumable (duplicates are ignored) and supports --letters for incremental syncing. Word lookups work whether or not the list has been synced.
  • Cambridge's browse index lists headwords only (and even its entry pages only show a few irregular inflections), so regular forms such as cooks/cooked/cooking would otherwise never appear. After syncing, beda expands every stored base lemma into its inflected forms using a bundled offline database — no extra scraping. Run beda words inflect to expand an already-synced database (e.g. one populated before this feature existed); it is idempotent, so it is safe to re-run. Only base lemmas are expanded — words that are themselves an inflection of another word (e.g. the gerund cooking) are skipped to avoid dubious second-order forms like cookings.
  • The inflection data is the Automatically Generated Inflection Database (AGID) by Kevin Atkinson, bundled (gzip-compressed and embedded via go:embed) at internal/inflect/data/. It is redistributed under its permissive licence; the full copyright, licence and source notices are in internal/inflect/data/AGID-README.txt and must accompany any copy of this data.
  • The scraper sends browser-like headers, retries transient failures (429/403/5xx) with exponential backoff and jitter (honouring Retry-After), and rate-limits to stay polite. Please respect Cambridge Dictionary's terms of use.
  • Cambridge rate-limits aggressive scraping. If a range page keeps failing after retries, words sync skips it (rather than aborting) and prints a summary of how many ranges were skipped; simply re-run the sync to pick them up. If you see many skips, lower --workers and/or raise --delay.
  • Go dependencies are vendored (vendor/), so the Docker image builds with no network access (GOFLAGS=-mod=vendor GOPROXY=off). This avoids proxy.golang.org connectivity issues on constrained hosts such as a Raspberry Pi. After changing dependencies, run go mod tidy && go mod vendor.
  • This project is for educational use; it is not affiliated with Cambridge University Press.