LOINC Code Lookup API: Search Lab Tests and Build FHIR Observations
LOINC identifies every lab test and clinical observation in healthcare. Here's how to look up codes, search by component and specimen, read the six-axis model, and drop the result into a FHIR Observation — with working TypeScript.

If your application touches lab results, it speaks LOINC. Every glucose level, every CBC, every culture result that moves between systems carries a LOINC code to say what was measured. It's the Observation.code half of the FHIR lab story — the other half being the result value and its units.
Working with LOINC directly is its own project. Regenstrief ships it as a set of CSV tables you have to download, accept a license for, parse, and reload a few times a year. This tutorial shows how to look up, search, and batch-process LOINC codes with FHIRfly's API and TypeScript SDK — and how to turn a lookup into a FHIR Observation.
What Makes LOINC Different
Unlike ICD-10 or NDC, a LOINC code isn't a flat label — it's a coordinate in a six-axis model. Each code is defined by its parts:
| Part | Meaning | Example (2345-7) |
|---|---|---|
| Component | What is measured | Glucose |
| Property | Kind of quantity | MCnc (mass concentration) |
| Time | Point vs interval | Pt (point in time) |
| System | Specimen / sample type | Ser/Plas (serum or plasma) |
| Scale | Quantitative, ordinal, nominal | Qn |
| Method | How it was measured (optional) | null |
This is why "glucose" alone isn't enough — glucose in blood, in serum/plasma, in urine, and after a glucose challenge are all distinct codes. The parts are what let you disambiguate, and FHIRfly returns them as structured fields you can filter and read directly.
Setup
npm install @fhirfly-io/terminology
import { Fhirfly } from "@fhirfly-io/terminology";
const fhirfly = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY! });
Free tier includes 10,000 requests/month — get your key here.
Look Up a Single Code
The most common operation: you have a LOINC code and need the name, parts, and units.
const result = await fhirfly.loinc.lookup("2345-7", { shape: "standard" });
console.log(result.data.code);
// "2345-7"
console.log(result.data.display_name);
// "Glucose, Blood"
console.log(result.data.long_name);
// "Glucose [Mass/volume] in Serum or Plasma"
console.log(result.data.parts.system);
// "Ser/Plas"
console.log(result.data.units.example_ucum_units);
// "mg/dL"
console.log(result.data.order_obs);
// "Both" — orderable AND reportable
The order_obs field is worth noting: it tells you whether a code is meant to be ordered (Order), reported as a result (Observation), or Both. Use it to keep order-entry pick-lists separate from result-display logic.
Response Shapes
| Shape | Fields | Best for |
|---|---|---|
compact | Code, display name, shortname, class, component | Autocomplete, search results |
standard | + Parts, units, status, order/obs, map-to, FHIR coding | Result processing, FHIR mapping |
full | + Consumer name, version history, common-test ranks, source/copyright | AI agents, auditing, provenance |
FHIR-Ready Coding
Every response includes a pre-built FHIR coding:
const result = await fhirfly.loinc.lookup("718-7", { shape: "standard" });
console.log(result.data.fhir_coding);
// {
// system: "http://loinc.org",
// code: "718-7",
// display: "Hemoglobin, Blood"
// }
Drop this straight into an Observation.code — more on that below.
Search by Term, Component, and Specimen
Most LOINC work starts from a description, not a code. Search accepts a free-text q plus structured filters that map to the six axes.
const results = await fhirfly.loinc.search({ q: "glucose" }, { limit: 5 });
console.log(`${results.total} codes match "glucose"`);
// 976 codes match "glucose"
for (const code of results.items) {
console.log(`${code.code} — ${code.display_name} (${code.class})`);
}
// "51595-7 — Glucose, Stool (CHEM)"
// "2349-9 — Glucose, Urine (CHEM)"
// "18296-4 — Glucose after dose glucose, Blood (CHAL)"
// "47622-6 — Glucose before dose glucose, Blood (CHAL)"
// "2339-0 — Glucose, Blood (CHEM)"
Nearly a thousand hits for one analyte — exactly why the axis filters matter. Narrow by specimen system, LOINC class, or scale:
// Hemoglobin measured in blood
await fhirfly.loinc.search({ q: "hemoglobin", system: "Bld" });
// 55235-6 (gene panel), 717-9, 718-7, 59260-0, ...
// Note the top hit is a panel, not a plain measurement — add scale: "Qn" to drop it.
// Only quantitative chemistry tests on serum
await fhirfly.loinc.search({
class: "CHEM",
system: "Ser/Plas",
scale: "Qn",
});
// Only orderable codes (for an order-entry picker)
await fhirfly.loinc.search({ q: "lipid panel", order_obs: "Order" });
// Sort alphabetically instead of by relevance
await fhirfly.loinc.search({ q: "creatinine" }, { sort: "name" });
Pagination
let page = 1;
let hasMore = true;
while (hasMore) {
const results = await fhirfly.loinc.search(
{ class: "CHEM", scale: "Qn" },
{ limit: 50, page }
);
for (const code of results.items) {
await indexCode(code);
}
hasMore = results.has_more;
page++;
}
Build a FHIR Observation
This is where LOINC earns its keep. A lab result in FHIR is an Observation whose code is a LOINC coding and whose valueQuantity carries the number and UCUM unit. FHIRfly hands you both pieces — the coding and an example UCUM unit — from a single lookup:
async function buildGlucoseObservation(value: number, patientId: string) {
const loinc = await fhirfly.loinc.lookup("2345-7", { shape: "standard" });
return {
resourceType: "Observation",
status: "final",
code: {
coding: [loinc.data.fhir_coding], // { system: "http://loinc.org", code, display }
text: loinc.data.display_name,
},
subject: { reference: `Patient/${patientId}` },
valueQuantity: {
value,
unit: loinc.data.units.example_units ?? undefined, // "mg/dL"
system: "http://unitsofmeasure.org",
code: loinc.data.units.example_ucum_units ?? undefined, // "mg/dL"
},
};
}
const obs = await buildGlucoseObservation(99, "123");
The example_ucum_units field is the bridge to a valid UCUM-coded valueQuantity. It reflects how the test is typically reported — confirm against your own result units before relying on it for conversion, since some analytes are reported in more than one unit.
Batch Lookups
Enriching a result feed or a lab interface with hundreds of codes? Use the batch endpoint:
const response = await fhirfly.loinc.lookupMany(
["2345-7", "718-7", "2160-0", "0000-0"],
{ shape: "standard" }
);
for (const result of response.results) {
if (result.status === "ok") {
console.log(`${result.input}: ${result.data.display_name}`);
} else {
console.log(`${result.input}: ${result.status}`);
}
}
// "2345-7: Glucose, Blood"
// "718-7: Hemoglobin, Blood"
// "2160-0: Creatinine, Blood"
// "0000-0: not_found"
Batch accepts up to 100 codes per request. Each result carries its own status, so an invalid or retired code returns not_found without failing the whole batch — handy for validating an incoming feed.
Provenance and Ranking (Full Shape)
The full shape adds fields built for auditing and for prioritizing the tests that actually matter:
const result = await fhirfly.loinc.lookup("2345-7", { shape: "full" });
console.log(result.data.consumer_name);
// "Glucose, Blood" — patient-friendly label
console.log(result.data.ranks);
// { common_test_rank: 6, common_order_rank: 120 }
console.log(result.data.version);
// "2.82" — the LOINC release this term came from
common_test_rank reflects how frequently a code appears in real lab data (lower = more common). Use it to push the everyday tests to the top of an autocomplete and bury the obscure ones.
A Licensing Note
LOINC is free to use, but it is not public domain — it ships under the Regenstrief LOINC license, which requires attribution. FHIRfly surfaces this in every response so you don't have to track it separately:
const result = await fhirfly.loinc.lookup("2345-7", { shape: "standard" });
console.log(result.meta.legal.license);
// "regenstrief_license"
console.log(result.meta.legal.attribution_required);
// true
If you display LOINC names to end users, include the standard attribution. The full shape returns the exact citation string and terms-of-use link to make that straightforward.
REST API (Without the SDK)
# Single lookup
curl -H "x-api-key: $FHIRFLY_API_KEY" \
"https://api.fhirfly.io/v1/loinc/2345-7?shape=standard"
# Search
curl -H "x-api-key: $FHIRFLY_API_KEY" \
"https://api.fhirfly.io/v1/loinc/search?q=glucose&system=Bld&limit=10"
# Batch
curl -X POST -H "x-api-key: $FHIRFLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"codes": ["2345-7", "718-7", "2160-0"]}' \
"https://api.fhirfly.io/v1/loinc/_batch?shape=standard"
Common Use Cases
| Use Case | Approach |
|---|---|
| Lab result enrichment | Batch lookup with standard shape |
| Test search in an LIS/EHR | Search with axis filters, compact shape |
| Order-entry pick-lists | Search with order_obs: "Order" |
| FHIR Observation resources | Lookup, use fhir_coding + units |
| Code validation | Batch lookup, check for not_found status |
| Patient-facing labels | Lookup with full shape, read consumer_name |
| Prioritized autocomplete | Read ranks.common_test_rank |
Key Takeaways
- Six-axis model — component, property, time, system, scale, method are returned as structured, filterable fields
- Axis filters — narrow a thousand "glucose" hits down to the right specimen and scale
- FHIR + UCUM ready —
fhir_codingandexample_ucum_unitsbuild a validObservationin one call order_obs— separates orderable codes from reportable results- Common-test ranks — surface the tests clinicians actually use
- Batch endpoint — up to 100 codes, per-code status
- Attribution built in — Regenstrief license info travels with every response
Next Steps
- Get a free API key from the FHIRfly dashboard
- Install the SDK:
npm install @fhirfly-io/terminology - Search for the tests in your lab catalog and check their parts
- Read the LOINC API docs for the complete parameter reference
Build it on real terminology
Try any endpoint live — no sign-up required.
