Generate a typed client from the OpenAPI document
Generate a TypeScript or Python client from the OpenAPI document when you want the compiler to check paths, query parameters and response fields, and regenerate it when the document changes. Brellium also publishes official TypeScript and Python SDKs (Clients and SDKs). https://api.brellium.com/v2/openapi.json is the OpenAPI 3.1 document of the running API, and the API reference is built from it, so a generated client matches the API you call. A generated client adds no token caching, 429 handling or pagination. The hand-written client shows the code for each.
Prerequisites
- An access token in
$BRELLIUM_ACCESS_TOKEN, cached and reused (Authenticate with OAuth2 client credentials). Step 3 sends it, and the download in step 1 needs no token. - An
audit_id, for example one that the list call in step 3 returns. This page uses the sample audita1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d. - With a live key the response contains protected health information (PHI); a sandbox key returns sample audits.
- Node.js 20 or later (or Bun) and npm for the TypeScript client, or Python 3.9 or later and Docker for the Python client. The Docker image bundles the Java that
openapi-generatorruns on.
Steps
Download the document
Save the document as openapi.json, commit it and generate from that file instead of the live URL. Every run uses the same input, and you can diff the next download against it. The request returns the document as JSON, shortened in the response below.
curl "https://api.brellium.com/v2/openapi.json" \
--output openapi.json
import { writeFile } from "node:fs/promises";
const res = await fetch("https://api.brellium.com/v2/openapi.json");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
await writeFile("openapi.json", await res.text());
{
"openapi": "3.1.0",
"info": { "title": "Brellium API", "version": "2.0.0", "description": "..." },
"servers": [{ "url": "https://api.brellium.com" }],
"components": {
"securitySchemes": {
"OAuth2": {
"type": "oauth2",
"flows": {
"clientCredentials": {
// the token endpoint, see /guides/authentication
"tokenUrl": "https://api.brellium.com/v2/oauth/token",
"scopes": {}
}
}
}
},
"schemas": {
/* one schema per name: Audit, AuditList, Error, QuestionSet, User, ... */
}
},
"paths": { /* one entry per route under /v2/*, keyed by path then method */ },
"tags": [
{ "name": "Documents" },
{ "name": "Audits" },
/* ... */ { "name": "Email recipients" }
]
}
Redocly and Spectral report no errors in the downloaded document.
Generate the client
For TypeScript, openapi-typescript writes a .d.ts file of types only, and openapi-fetch, a 6 kB wrapper around fetch, checks each call against it by literal path and method. For Python, openapi-generator writes a full package with one method per operation. The outputs below come from openapi-typescript 7.9.1, openapi-fetch 0.17.0, TypeScript 5.9.3, openapi-generator 7.19.0 and Python 3.12.
npm install openapi-fetch@0.17.0
npm install --save-dev openapi-typescript@7.9.1 typescript@5.9.3
npx openapi-typescript openapi.json -o brellium.d.ts
✨ openapi-typescript 7.9.1
🚀 openapi.json → brellium.d.ts [68.3ms]
The file exports paths (paths["/v2/audits"]["get"]), operations keyed by operationId (operations["getAudit"]) and components (components["schemas"]["Audit"]). Commit brellium.d.ts next to openapi.json.
docker run --rm \
--volume "$PWD:/local" \
openapitools/openapi-generator-cli:v7.19.0 generate \
-i /local/openapi.json \
-g python \
-o /local/brellium-python \
--additional-properties=packageName=brellium
pip install ./brellium-python
[main] INFO o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO o.o.codegen.TemplateManager - writing file /local/brellium-python/brellium/api/audits_api.py
...
The package has one class per tag and a pydantic model for each schema. Each method name is the operation's operationId in snake_case. For example, GET /v2/audits/{audit_id} has the operationId getAudit, so its method is AuditsApi.get_audit. With -g typescript-fetch, the same generator names it AuditsApi.getAudit.
List audits and fetch one
The requests below list complete audits, then fetch one audit by an audit_id the list returned.
curl "https://api.brellium.com/v2/audits?status=complete&limit=5" \
--header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const params = new URLSearchParams({ status: "complete", limit: "5" });
const res = await fetch(`https://api.brellium.com/v2/audits?${params}`, {
headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
if (!res.ok)
throw new Error(
`HTTP ${res.status}: ${JSON.stringify((await res.json()).error)}`,
);
console.log(await res.json());
import BrelliumClient from "@brellium/api-client";
const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });
const audits = await client.audits.list({ status: "complete", limit: 5 });
console.log(audits);
import os
from brellium_api_client import BrelliumClient
client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])
audits = client.audits.list(status="complete", limit=5)
print(audits.to_json())
{
"object": "list",
"data": [
{
"audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"object": "audit" /* ... the body below */
} // the audit_id for the next call
],
"has_more": false
}
curl "https://api.brellium.com/v2/audits/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \
--header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const res = await fetch(
"https://api.brellium.com/v2/audits/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
{
headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
},
);
if (!res.ok)
throw new Error(
`HTTP ${res.status}: ${JSON.stringify((await res.json()).error)}`,
);
console.log(await res.json());
import BrelliumClient from "@brellium/api-client";
const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });
const audit = await client.audits.retrieve("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
console.log(audit);
import os
from brellium_api_client import BrelliumClient
client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])
audit = client.audits.retrieve("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d")
print(audit.to_json())
{
"audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"object": "audit",
// trust score and flags only when this is "complete"
"status": "complete",
"score": 0.6666666666666666,
// GET /v2/question_sets/{set_id}
"set_id": "3c2b1a09-8f7e-4d6c-b5a4-938271605f4e",
// document ids; fetch each from its own endpoint
"documents": ["0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b"]
// ... flags, created_at, updated_at, reviewed_at, display_info, people and
// responses
}
The scripts below make the same two calls through each generated client. Both clients send the token you give them as Authorization: Bearer <token>, and neither mints the token, retries a 429 or loops on has_more.
// Keep credentials on the server; never in a browser or mobile app.
import createClient from "openapi-fetch";
// "./brellium" under moduleResolution "bundler"
import type { paths } from "./brellium.js";
const token = process.env.BRELLIUM_ACCESS_TOKEN;
if (!token) throw new Error("BRELLIUM_ACCESS_TOKEN is not set");
const brellium = createClient<paths>({
baseUrl: "https://api.brellium.com",
headers: { Authorization: `Bearer ${token}` },
});
async function main() {
// Typed from the document: limit must be a number and status a string.
const list = await brellium.GET("/v2/audits", {
params: { query: { status: "complete", limit: 5 } },
});
if (list.error)
throw new Error(
`GET /v2/audits: ${list.response.status} ${list.error.error.code}`,
);
console.log(
list.data.has_more,
list.data.data.map((audit) => audit.audit_id),
);
// The path parameter is typed too; data is components["schemas"]["Audit"].
const one = await brellium.GET("/v2/audits/{audit_id}", {
params: { path: { audit_id: "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" } },
});
if (one.response.status === 404) {
console.log("audit not found");
} else if (one.error) {
throw new Error(
`GET /v2/audits/{audit_id}: ${one.response.status} ${one.error.error.code}`,
);
} else {
console.log(
one.data.status,
one.data.score,
one.data.set_id,
one.data.documents,
);
}
}
main();
false [ 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d' ]
complete 0.6666666666666666 3c2b1a09-8f7e-4d6c-b5a4-938271605f4e [ '0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b' ]
Every call returns { data, error, response }, where data is the 200 body, error is the Error envelope for any other status, and response is the raw Response. A wrong type such as limit: "5" fails to compile. A misspelled query parameter compiles, and openapi-fetch sends it as it is, so check parameter names against the document.
# Keep credentials on the server; never in a browser or mobile app.
import os
import brellium
from brellium.rest import ApiException
configuration = brellium.Configuration(host="https://api.brellium.com")
configuration.access_token = os.environ["BRELLIUM_ACCESS_TOKEN"]
with brellium.ApiClient(configuration) as api_client:
audits = brellium.AuditsApi(api_client)
# Query parameters are keyword arguments; limit is validated as 1..100 before
# the request is sent.
page = audits.list_audits(status="complete", limit=5)
print(page.has_more, [str(audit.audit_id) for audit in page.data])
try:
audit = audits.get_audit("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d")
print(
audit.status,
audit.score,
str(audit.set_id),
[str(doc) for doc in audit.documents],
)
except ApiException as e:
if e.status == 404:
print("audit not found")
else:
raise
False ['a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d']
complete 0.6666666666666666 3c2b1a09-8f7e-4d6c-b5a4-938271605f4e ['0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b']
The client sends configuration.access_token as an Authorization: Bearer header. A non-2xx status raises ApiException, which carries .status, .reason and the error envelope in .body. Fields with format: uuid come back as uuid.UUID, so the sample wraps them in str().
Both scripts handle 404 NOT_FOUND, which the retrieve call returns when your organization has no audit with this id. Another organization's id and a deleted audit return 404 too.
An id that is not a UUID returns 400 VALIDATION_ERROR. Either call returns 401 UNAUTHORIZED when the token is missing, invalid or expired, and 429 RATE_LIMITED when your organization sends too many requests.
Regenerate when the document changes
The API adds query parameters, response fields and new values for string fields without notice (Versioning and compatibility). A generated client includes them only after you regenerate it. The commands below download the document again, show what changed and regenerate the TypeScript types.
curl "https://api.brellium.com/v2/openapi.json" \
--output openapi.json
git diff --stat openapi.json
npx openapi-typescript openapi.json -o brellium.d.ts
A new response field is harmless, because openapi-fetch passes the body through and the pydantic models copy only known fields. Until you regenerate, passing a new query parameter raises a ValidationError in Python, and openapi-fetch sends it with no type check. To keep the client current, run a weekly job that opens a pull request when the diff is not empty.
Expected response
Both generated clients deserialize the same GET /v2/audits/{audit_id} body (Retrieve an audit), as components["schemas"]["Audit"] in TypeScript and as brellium.Audit in Python. For the full body, see Retrieve an audit by id, and for every field, see Understand an audit. Fetch each document the audit lists with Retrieve a document.
Common mistakes
| Mistake | What you see | Fix |
|---|---|---|
| Committing a token into the generated package | configuration.access_token = "eyJ..." in a committed file |
Read BRELLIUM_ACCESS_TOKEN from the environment. A committed token leaks even though it expires after expires_in. |
| Guessing a path-derived method name | Python: AttributeError: 'AuditsApi' object has no attribute 'v2_audits_get' |
Use the names that come from the operationId, such as list_audits and get_audit in Python, or listAudits and getAudit with -g typescript-fetch. openapi-fetch has no per-operation methods and takes the literal path instead. |
Passing include_total: true |
TypeScript: Type 'boolean' is not assignable to type 'string'; Python: pydantic ValidationError |
Pass include_total: "true", because the parameter is a string in the document. |
Next
Clients and SDKs
Use an official SDK, call the API over HTTPS, generate a typed client, or start from a minimal hand-written client.
Mint one access token and reuse it
Mint a token once, cache it until expires_in, re-mint on 401, wait on 429.
Versioning and compatibility
What v2 may change without notice, what it will not, and how to write a client that survives both.