Writing a fetch wrapper for a REST API is boring, error-prone, and almost always out of date within a release cycle. Yet most Solana tutorials still start with fetch(url, { headers: { "x-api-key": ... } }) copy-paste boilerplate, and every consumer ends up hand-typing response shapes into ad-hoc interfaces.
There's a shorter path: if the API ships an OpenAPI 3.x spec, you can generate a fully typed client in less time than it takes to read its documentation. MadeOnSol publishes its spec at /api/v1/openapi.json and renders an interactive reference via Scalar. This post shows how to use both to go from "I want to call this API" to "I have a typed client in my project" in about five minutes.
What you get out of codegen
Before the how-to, a concrete before/after so the value is clear.
Without codegen, every endpoint call looks like this:
const res = await fetch(
"https://madeonsol.com/api/v1/kol/leaderboard?period=24h&limit=50",
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
const data = await res.json();
// data is `any`. Typos in field names compile. Type drift at runtime.
With codegen, it looks like this:
const { data, error } = await client.GET("/v1/kol/leaderboard", {
params: { query: { period: "24h", limit: 50 } },
});
// data is fully typed. IntelliSense shows every field.
// Wrong param names fail at compile time.
The difference matters most when the API evolves. A renamed field in the spec becomes a red squiggle in your IDE instead of a silent undefined at 2am.
Step 1: Fetch the spec
MadeOnSol exposes its OpenAPI 3.1 spec at a single URL. Download it to your project:
curl https://madeonsol.com/api/v1/openapi.json -o openapi.json
You can also inspect it interactively first at the public API reference — which is itself generated from the same spec via Scalar. If the endpoint you want to use isn't documented there, it isn't in the spec, and codegen won't emit it.
Step 2: Install the codegen
openapi-typescript is the lightest and most maintained option. It parses the spec and emits a single .ts file of type definitions — no runtime dependency, no SDK lock-in.
npm install --save-dev openapi-typescript
npm install openapi-fetch
The second package, openapi-fetch, is a 3kb runtime wrapper that reads the generated types and gives you a type-safe GET / POST / etc. client. It's the minimal runtime you actually need.
Step 3: Generate the types
Add a script to package.json:
{
"scripts": {
"generate:api": "openapi-typescript openapi.json -o src/api-types.ts"
}
}
Run it:
npm run generate:api
This produces src/api-types.ts with paths, components, and operations interfaces describing every endpoint in the spec. You won't typically read this file — it's for the compiler. Expect it to be a few hundred KB depending on how many endpoints the API has.
Step 4: Create the client
A single file, usually src/client.ts:
import createClient from "openapi-fetch";
import type { paths } from "./api-types";
export const client = createClient<paths>({
baseUrl: "https://madeonsol.com/api",
headers: {
Authorization: `Bearer ${process.env.MADEONSOL_API_KEY}`,
},
});
That's the whole client. Every call made through it is type-checked against the spec at compile time.
Step 5: Call endpoints with full type safety
import { client } from "./client";
async function topKols() {
const { data, error, response } = await client.GET("/v1/kol/leaderboard", {
params: { query: { period: "24h", limit: 20 } },
});
if (error) {
console.error(`API error ${response.status}:`, error);
return;
}
// data.kols is fully typed — wallet, name, win_rate, pnl_sol, etc.
for (const kol of data.kols) {
console.log(`${kol.name} — ${(kol.win_rate * 100).toFixed(1)}% win rate`);
}
}
A few things worth noting about the ergonomics:
{ data, error } — the client returns both, instead of throwing on non-2xx. This makes error handling explicit in the type system.
params.query — query string parameters are nested under query; path params under path; request bodies go to body. If the endpoint doesn't take a query string, TypeScript will block you from passing one.
response — the raw Response object is always available for header inspection (rate-limit counters, trace IDs, etc.).
Step 6: Keep the client in sync
Regenerate whenever the API ships a new version:
curl https://madeonsol.com/api/v1/openapi.json -o openapi.json
npm run generate:api
A useful CI step is to commit openapi.json alongside api-types.ts and fail the build if they drift. That way, a breaking API change shows up as a PR diff you can review rather than a production surprise. GitHub Actions example:
- name: Check API types are fresh
run: |
curl https://madeonsol.com/api/v1/openapi.json -o openapi.json.new
diff -q openapi.json openapi.json.new
Handling rate-limit headers
MadeOnSol returns X-RateLimit-Remaining and X-RateLimit-Reset on every response. The codegen client exposes the raw response object, so you can pull headers without leaving the type-safe API:
const { data, response } = await client.GET("/v1/kol/feed", {
params: { query: { limit: 50 } },
});
const remaining = Number(response.headers.get("x-ratelimit-remaining"));
const resetAt = Number(response.headers.get("x-ratelimit-reset")); // unix seconds
if (remaining < 10) {
const sleepMs = (resetAt * 1000) - Date.now();
await new Promise((r) => setTimeout(r, Math.max(0, sleepMs)));
}
This pattern — check remaining, back off until reset — is the only rate-limit handling most scripts need. No custom retry library required. A typed client like this pairs well with long-running jobs such as monitoring Solana wallet activity with the Wallet Tracker API, where compile-time safety on every endpoint saves you from silent field drift.
When to use codegen vs a vendor SDK
Both approaches are valid. A rough guide:
- Use the vendor SDK (MadeOnSol ships an
madeonsol npm package) when you want convenience methods, curated helpers, and a stable surface that might abstract over multiple underlying endpoints. SDKs trade fidelity for ergonomics.
- Use codegen when you want every endpoint, every parameter, and every response field exactly as the server exposes them — with zero chance the SDK lags behind a new release. Codegen is also the right choice if you're writing something internal, generating multiple clients in different languages, or building tooling that needs to introspect the spec itself.
For most apps, we'd pick the SDK for the 80% of calls that need convenience, and drop to codegen only when we need an endpoint that isn't wrapped yet. Both can coexist in the same project. Once you have a typed client wired up, it's the foundation for richer projects — our walkthrough on building a Solana token analytics dashboard shows how to combine price, holder, volume, and liquidity calls into a single view.
Troubleshooting common issues
any types everywhere in generated file. Usually means the spec uses additionalProperties: true or has unnamed oneOf fields. Pass --immutable --alphabetize to the codegen and it will resolve more cleanly.
- Response body is
undefined at runtime. Check the error field — a non-2xx response returns undefined on data and the parsed error body on error. This is intentional and correct; don't destructure before checking .