@knorby/openfda-client
v0.1.0
Published
A fully-typed, zero-dependency TypeScript client for the openFDA API. Universal: Node, React Native, browsers, Bun, and Deno.
Maintainers
Readme
@knorby/openfda-client
A fully-typed, zero-dependency TypeScript client for the
openFDA API — search FDA public data on drugs,
devices, foods, cosmetics, tobacco, and more. Universal: works in Node,
React Native (Expo), browsers, Bun, and Deno — anywhere the standard Web
fetch is available.
Disclaimer: This library is an independent, open-source project and is not affiliated with, endorsed by, or sponsored by the U.S. Food and Drug Administration (FDA) or the U.S. Government. "openFDA" is an FDA research project; this package is only a third-party API client for it.
- Zero runtime dependencies — built on the standard
fetch,Headers,AbortController, andResponse(all global in modern runtimes). - Every endpoint, one interface — all openFDA endpoints share one query
surface (
search/count/limit/skip/sort), so every one of the 30 live endpoints is reachable through the same client — typed namespaces for the high-value ones, generic paths for everything else. - Fully typed where it matters — drug (events, labels, NDC directory, Drugs@FDA, Orange Book, enforcement, shortages), food, and cosmetic endpoints ship with hand-written response models; every other endpoint is a caller-typable generic.
- Auto-pagination —
searchAll()async iterators walk pages for you and respect the API's 25,000-record paging ceiling. - Predictable errors — typed
OpenFdaApiError/OpenFdaTimeoutErrorwith parsedRetry-Afteron rate-limit (429) responses, and a dedicatedOpenFdaNotFoundErrorfor openFDA's distinctive "zero matches = 404" behavior. - Drift-resilient — a scheduled GitHub Actions job diffs the client's endpoint registry and field-shape snapshots against the live API and opens a review PR when FDA changes something.
Data and medical disclaimers
openFDA's own warning applies to all data retrieved through this client:
Do not rely on openFDA to make decisions regarding medical care. While we make every effort to ensure that data is accurate, you should assume all results are unvalidated.
- Not all openFDA data has been validated for clinical or production use. Treat every result as unvalidated.
- Adverse-event reports are voluntary. Drug (FAERS), food, cosmetic, and device event reports do not establish causation, can be incomplete or inaccurate, and must not be used to estimate incidence.
- Data is public domain (CC0 1.0) unless otherwise noted on open.fda.gov. openFDA asks (but does not require) attribution: "Data provided by the U.S. Food and Drug Administration (https://open.fda.gov)".
- Some device data includes GMDN® content licensed from The GMDN Agency, which carries its own usage restrictions — see the openFDA terms.
Install
npm install @knorby/openfda-clientQuick start
import { OpenFdaClient } from "@knorby/openfda-client";
// No API key needed (240 req/min, 1,000 req/day per IP);
// a free key raises this to 120,000 req/day.
const client = new OpenFdaClient({
// apiKey: process.env.OPENFDA_API_KEY,
});
// Typed namespaces for the priority endpoints
const labels = await client.drug.label.search({
search: 'openfda.brand_name:"advil"',
limit: 5,
});
// Facet counts (unique values of a field)
const reactions = await client.drug.event.count({
count: "patient.reaction.reactionmeddrapt.exact",
limit: 10,
});
// A count term may be a string or a number, depending on the field.
// Lazily iterate every matching record across pages (stops at the 25k cap)
for await (const recall of client.food.enforcement.searchAll({
search: "status:Ongoing",
})) {
console.log(recall.recalling_firm, recall.reason_for_recall);
}Composing searches
Search expressions are openFDA's Elasticsearch-style syntax. Build them with the bundled helpers instead of hand-escaping:
import { and, exact, field, range } from "@knorby/openfda-client";
const search = and(
exact("openfda.brand_name", "ADVIL"),
range("receivedate", { gte: "20240101", lte: "20241231" }),
);
// 'openfda.brand_name.exact:ADVIL AND receivedate:[20240101 TO 20241231]'
const events = await client.drug.event.search({ search, limit: 10 });Every endpoint, even untyped ones
All nine live API nouns are exposed as namespaces (client.drug,
client.food, client.cosmetic, client.device, client.tobacco,
client.animalandveterinary, client.other, client.research,
client.transparency). Untyped endpoints surface generic records:
const cls = await client.device.classification.search({ limit: 1 });
cls.results[0]?.device_name; // typed models land as FDA data stabilizes
// Any endpoint — including brand-new ones FDA hasn't announced — via the
// generic path methods (new endpoints work without a client release):
const crl = await client.search("transparency/crl", { limit: 1 });
const custom = await client.search<{ my_field: string }>("future/endpoint");
const byUdi = await client.device["510k"].search({ limit: 1 }); // digit-leading keys use bracket accessZero matches is a 404
openFDA reports a search with zero matching records as HTTP 404
({"error":{"code":"NOT_FOUND"}}), not as an empty array. The client
surfaces this as a typed error you can catch:
import { OpenFdaNotFoundError } from "@knorby/openfda-client";
try {
await client.drug.label.search({ search: 'openfda.brand_name:"nope"' });
} catch (err) {
if (err instanceof OpenFdaNotFoundError) {
// treat as "no matches" (e.g. resolve to [])
} else {
throw err;
}
}Universal runtime notes
The client uses the global fetch (and Headers / AbortController /
Response), which is native in:
| Runtime | Available since | | -------------- | ------------------------ | | Node.js | 18 | | Browsers | Evergreen | | React Native | 0.73+ (fetch polyfill) | | Bun / Deno | All |
For tests or older runtimes, inject a custom fetch:
const client = new OpenFdaClient({ fetch: (url, init) => myFetchImpl(url, init) });Endpoint coverage
| Namespace member | Dataset | Typed model |
| --------------------------------- | ---------------------------------------- | ----------- |
| client.drug.event | Drug adverse events (FAERS) | DrugEvent |
| client.drug.label | Structured product labeling | DrugLabel |
| client.drug.ndc | NDC directory | DrugNdc |
| client.drug.enforcement | Drug recall enforcement reports | DrugEnforcement |
| client.drug.drugsfda | Drugs@FDA applications | DrugsFda |
| client.drug.orangebook | Orange Book approvals | DrugOrangeBook |
| client.drug.shortages | Drug shortages | DrugShortage |
| client.food.event | Food adverse events | FoodEvent |
| client.food.enforcement | Food recall enforcement reports | FoodEnforcement |
| client.cosmetic.event | Cosmetic adverse events | CosmeticEvent |
| client.device.* (9 endpoints) | 510(k)s, PMA, classification, events, recalls, registration & listing, UDI, COVID-19 serology | generic |
| client.tobacco.* (4 endpoints) | Problem reports + research datasets | generic |
| client.animalandveterinary.event| Animal-drug adverse events | generic |
| client.other.nsde, .historicaldocument | NSDE, historical documents | generic |
| client.other.substance | GSRS substance records | Substance |
| client.other.unii | UNII substance-name crosswalk | UniiRecord |
| client.research.covidmirnaandproteomics | COVID-19 miRNA/proteomics | generic |
| client.transparency.crl | Complete Response Letters | generic |
Every endpoint object exposes search(params), count(params), and
searchAll(params, pageSize?). Generic access works for all paths via
client.search("noun/endpoint", …).
Rate limits and authentication
| Mode | Rate limit | | ------------- | --------------------------------- | | No API key | 240 requests/min · 1,000 requests/day (per IP) | | Free API key | 240 requests/min · 120,000 requests/day (per key) |
Request a free key at open.fda.gov
and pass it via new OpenFdaClient({ apiKey }). The key is appended as the
api_key query parameter and redacted from client-generated error
diagnostics, including API response bodies and sanitized transport causes.
On 429 responses, OpenFdaApiError.retryAfterSeconds carries the
server's recommended back-off.
The client can also retry 429s for you — opt in with
new OpenFdaClient({ retryOn429: true }) (or { retryOn429: { maxRetries: n } },
where n is a nonnegative integer). Invalid retry counts throw RangeError.
It honors Retry-After (capped at 60s) and otherwise backs off
exponentially; see ADR-0004.
Paging limit
skip maxes out at 25,000 (with limit ≤ 1,000), so search pagination
covers ~26,000 records per query. searchAll() stops there automatically
and documents it; for bulk access use the official
download files.
Known API quirks
These other/substance (GSRS) search behaviors were mapped through live
probing during development. They are openFDA-side behaviors — openFDA's own
documentation does not call them out — and knowing them saves real
debugging time:
names.namesearches can dead-end. A value you can see in a record'snames[].namemay still return404 NOT_FOUNDwhen searched asnames.name:<value>(indexing/tokenization varies across records). A 404 here does not prove the record doesn't exist.other/uniiis the dependable crosswalk. For substance-name ↔ UNII resolution, queryclient.other.unii(UniiRecord:substance_name/unii) instead of searching substance names..exactfails on nestedname_orgsfields. Queries likenames.name_orgs.name_org.exact:…return404 NOT_FOUND. Drop the.exactmodifier on nested name-organization fields.- Some
names.name+name_orgscombinations return HTTP 500. Certainnames.name:… AND names.name_orgs.…queries fail with a bare server error rather than a structured error code. Narrow the name query first, and test each clause on its own before combining.
As everywhere in openFDA, a zero-match search surfaces as
404 NOT_FOUND — see Zero matches is a 404.
Keeping up with the API
scripts/discover-endpoints.mjs diffs the client's endpoint registry
against the live API manifest, and scripts/capture-shapes.mjs snapshots
every endpoint's field skeleton into tests/shapes/ — together with
npm run test:live these run weekly in the openFDA API drift workflow,
which opens a review PR when FDA adds, removes, or reshapes anything. Run
them yourself with npm run drift:check.
Development
See AGENTS.md for repository conventions and CONTRIBUTING.md for the development workflow.
npm install # does not run prepare (see .npmrc)
npx husky # set up git hooks
pre-commit run --all-files # optional full-repo validation
npm test # unit tests (no network)
npm run test:live # opt-in live smoke tests
npm run drift:check # verify endpoint registry + shape snapshotsLicense
Apache-2.0 © Kali Norby (@knorby)
This project is unaffiliated with the FDA; openFDA data is public domain (CC0 1.0) unless otherwise noted.
