- Go 98.7%
- Dockerfile 1.3%
| cmd/beda | ||
| internal | ||
| vendor | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
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.cook→cooks,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:
lengthcounts every character in the stored word, so hyphenated multi-word entries (e.g.x-ray) include the hyphen in their length. Addalpha=trueto 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 syncis resumable (duplicates are ignored) and supports--lettersfor 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/cookingwould otherwise never appear. After syncing,bedaexpands every stored base lemma into its inflected forms using a bundled offline database — no extra scraping. Runbeda words inflectto 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 gerundcooking) are skipped to avoid dubious second-order forms likecookings. - The inflection data is the Automatically Generated Inflection Database (AGID) by Kevin Atkinson, bundled (gzip-compressed and embedded via
go:embed) atinternal/inflect/data/. It is redistributed under its permissive licence; the full copyright, licence and source notices are ininternal/inflect/data/AGID-README.txtand 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 syncskips 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--workersand/or raise--delay. - Go dependencies are vendored (
vendor/), so the Docker image builds with no network access (GOFLAGS=-mod=vendor GOPROXY=off). This avoidsproxy.golang.orgconnectivity issues on constrained hosts such as a Raspberry Pi. After changing dependencies, rungo mod tidy && go mod vendor. - This project is for educational use; it is not affiliated with Cambridge University Press.