dictionaryapi.dev is the API behind nearly every "build a dictionary app" tutorial: keyless, free, open-source (meetDeveloper/freeDictionaryAPI), with Wiktionary-sourced definitions, phonetics, and pronunciation audio. It's run by a single maintainer on donations, and by the README's own account it serves more than 10 million requests a month. That's the context for what we measured while writing this guide (September 2026):
$ curl --max-time 15 "https://api.dictionaryapi.dev/api/v2/entries/en/hello"
curl: (28) Operation timed out after 15001 milliseconds with 0 bytes received
# with a bigger budget it does answer โ in twenty seconds:
$ curl -w " (%{http_code} in %{time_total}s)" ".../api/v2/entries/en/hello"
[{"word":"hello", โฆ }] (200 in 19.96s)
# and words the CDN hasn't cached don't answer at all:
$ curl -w " (%{http_code} in %{time_total}s)" ".../api/v2/entries/en/coffee"
error code: 522 (522 in 20.13s)
In our timed runs, every call took ~20 seconds. Words with a stale Cloudflare cache copy (cf-cache-status: STALE โ the hello we got had an age of 53 days) eventually returned 200; everything else โ including world and coffee โ came back as HTTP 522 with the plain-text body error code: 522. Note what that does to tutorial code: res.json() throws on that body, and the friendly documented 404 ("title":"No Definitions Found"โฆ) never gets a chance to appear. There's also a real rate limit โ the API's own headers advertise x-ratelimit-limit: 450, which matches the documented 450 requests per 5 minutes per IP.
None of this is a complaint. A free, keyless dictionary API maintained by one person is a genuinely kind thing to exist, and light production use (with caching) plus a donation is how it stays alive. But a dev server re-firing lookups on every keystroke, or a CI matrix querying it in parallel, is fragile for you and load for them. Point those at a mock that answers instantly with the exact shapes.
Capture the words your app and tests actually use (be patient โ give it a generous timeout), then reproduce the exact paths as custom routes. One curl creates a project, no signup:
curl -s --max-time 60 "https://api.dictionaryapi.dev/api/v2/entries/en/hello" > hello.json
curl -s --max-time 60 "https://api.dictionaryapi.dev/api/v2/entries/en/serendipity" > serendipity.json
curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects?name=dictionary-mock" | tee project.json
# โ {"id":"YOUR_ID","adminKey":"YOUR_ADMIN_KEY",โฆ}
jq '{path:"/api/v2/entries/en/hello", body:.}' hello.json \
| curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' --data-binary @-
jq '{path:"/api/v2/entries/en/serendipity", body:.}' serendipity.json \
| curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' --data-binary @-
Now GET /m/YOUR_ID/api/v2/entries/en/hello answers instantly with the array-of-entries shape your renderer expects โ word, phonetics (with audio URLs), meanings[].partOfSpeech, definitions[].definition, synonyms, antonyms, license, sourceUrls โ because it is the real response, replayed. We verified both served routes parse-equal to the live captures. Swap the base URL in your app config and nothing else changes.
When the API is healthy, a missing word answers a friendly JSON 404. Right now (see above) misses answer a plain-text 522 instead โ so your "no definitions" UI state may literally never have been exercised against the real shape. Make it a permanent fixture:
curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' \
-d '{"path":"/api/v2/entries/en/xyzzy","status":404,"body":{"title":"No Definitions Found","message":"Sorry pal, we could not find definitions for the word you were looking for.","resolution":"You can try the search again at later time or head to the web instead."}}'
One test looks up hello and renders definitions; another looks up xyzzy and asserts the empty-state UI. Both deterministic, both instant.
curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' \
-d '{"path":"/drills/522","status":522,"contentType":"text/plain","body":"error code: 522"}'
Point your lookup function at /m/YOUR_ID/drills/522 and assert it survives res.json() throwing on a non-JSON error body โ the exact bug this outage exposes.AbortSignal.timeout(2000)) and point it at ?mock_delay=5000 โ fetch throws with no response object, the same path a 20-second origin exercises. We verified curl exits 28 (timed out) on exactly this setup.?mock_seq=522,522,200 on the hello route fails exactly twice, then serves the real entry โ assert your retry/backoff renders on attempt three.?mock_ratelimit=3 gives three 200s with decrementing x-ratelimit-remaining, then a 429 with a real Retry-After to point your backoff at. More: rate-limit simulation, loading & error states.If you'd rather query than replay URLs, host the same captures as a resource โ records keep every nested field verbatim:
jq -s '{words: [.[][0]]}' hello.json serendipity.json > db.json
curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?name=dictionary-data" \
-H 'content-type: application/json' --data-binary @db.json
The import answers with honest warnings (nested arrays like meanings are kept verbatim in the records but skipped from the flat field schema), and you get the full query toolkit: ?word=serendipity exact-matches, ?q=greeting full-text-searches inside the nested definitions, ?select=word projects, writes persist, and /m/YOUR_ID/types.ts generates TypeScript interfaces from your records (?format=zod for Zod).
| dictionaryapi.dev | Your mock | |
|---|---|---|
| Real dictionary breadth | โ any English word, phonetics, audio | โ only the words you capture โ by design |
| Signup / key | none | none |
| Latency | ~20s per call in our Sep 2026 runs | edge-served, instant |
| Reliability | uncached words answered 522 in our runs | independent of their origin |
| Rate limit | 450 req / 5 min / IP | 10,000/day per project |
| Deterministic for tests | โ | โ |
| Can simulate 522 / stall / 429 / 404 | โ (only by accident) | โ routes + mock_delay/mock_seq/mock_ratelimit |
| Right for | light production use, cached โ and donate | dev, CI, demos, workshops |
The pattern that's fair to everyone: base URL in an env var โ dictionaryapi.dev in production behind a cache, your /m/YOUR_ID in .env.test and the dev server. The maintainer stops paying for your hot reloads; your CI stops inheriting their bad hours. Current status of the real API: /status/dictionaryapi. Related: Open Library, Quotable, mock any third-party API.