Generating a TypeScript Axios Client from OpenAPI
A typed HTTP client that stays in lockstep with your spec is the fastest way to stop frontend teams from hand-writing fetch calls. This guide shows the exact OpenAPI Generator invocation to produce a typescript-axios SDK, which --additional-properties actually matter, how to wire it into a build, and how to publish it to npm. It is part of SDK Generation & Changelog Automation and assumes you already have a valid OpenAPI 3.x document.
Problem & Context
Frontend developers consuming your API by hand-writing axios calls produce three recurring problems: request and response types drift from the real API, a renamed field breaks silently at runtime, and every team re-implements auth and error handling. Generating a typescript-axios client solves all three — it ships fully typed request methods, model interfaces, and a single Configuration object for base URL and credentials.
The friction is in the details. The typescript-axios generator has a long list of --additional-properties, and the wrong combination produces a package that will not compile, will not tree-shake, or cannot be published. The three that change the shape of the output most are:
npmName— the package name; without it, the generatedpackage.jsonis incomplete and not publishable.supportsES6— emits ES6 (import/export,const) instead of ES5, required for modern bundlers and tree-shaking.withSeparateModelsAndApi— splits models and APIs into separate directories so a model change does not invalidate every API import.
Step-by-Step Solution
1. Pin the generator version
npm install -g @openapitools/openapi-generator-cli
openapi-generator-cli version-manager set 7.6.0
openapi-generator-cli version
Expected output:
Did set the version to 7.6.0
7.6.0
2. List the generator’s properties (optional but recommended)
Confirm the property names for the version you pinned — they occasionally change between majors.
openapi-generator-cli config-help -g typescript-axios | grep -E "npmName|supportsES6|withSeparateModelsAndApi"
Expected output (truncated):
npmName
The name under which you want to publish generated npm package.
supportsES6
Generate code that conforms to ES6. (Default: false)
withSeparateModelsAndApi
Put the model and api in separate folders ... (Default: false)
3. Generate the client
openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-axios \
-o ./sdk \
--additional-properties=npmName=@acme/api-client,npmVersion=1.0.0,supportsES6=true,withSeparateModelsAndApi=true,useSingleRequestParameter=true
Expected output (truncated):
[main] INFO o.o.codegen.DefaultGenerator - Generated server response code: ...
[main] INFO o.o.codegen.TemplateManager - writing file ./sdk/api/default-api.ts
[main] INFO o.o.codegen.TemplateManager - writing file ./sdk/models/index.ts
[main] INFO o.o.codegen.TemplateManager - writing file ./sdk/package.json
[main] INFO o.o.codegen.DefaultGenerator - Generation took: ... ms
Confirm the package name and layout:
grep '"name"' ./sdk/package.json && ls ./sdk
Expected output:
"name": "@acme/api-client",
api base.ts common.ts configuration.ts index.ts models package.json tsconfig.json
4. Build the package
The generator emits TypeScript and a package.json whose build script runs tsc. Compile to JavaScript plus declaration files before doing anything else.
cd sdk
npm install
npm run build
ls dist
Expected output:
api.js api.d.ts configuration.js configuration.d.ts index.js index.d.ts models
5. Wire it into a consuming app
// usage.ts — in the app that depends on @acme/api-client
import { Configuration, DefaultApi } from '@acme/api-client';
const api = new DefaultApi(
new Configuration({
basePath: 'https://api.acme.com',
accessToken: () => localStorage.getItem('token') ?? '',
}),
);
const { data } = await api.listOrders();
console.log(data.length);
6. Publish to npm
cd sdk
npm publish --access public
Expected output:
+ @acme/[email protected]
Complete Working Example
A GitHub Actions workflow that regenerates, builds, and publishes the SDK whenever the spec changes — the full pipeline in one self-contained file.
# .github/workflows/publish-sdk.yml
name: Publish TypeScript SDK
on:
push:
branches: [main]
paths: ['openapi.yaml']
jobs:
generate-build-publish:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# Pin the generator so CI output is deterministic.
- name: Pin OpenAPI Generator
run: npx --yes @openapitools/openapi-generator-cli version-manager set 7.6.0
# Generate the typescript-axios client with the properties that shape the output.
- name: Generate SDK
run: |
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-axios \
-o ./sdk \
--additional-properties=npmName=@acme/api-client,npmVersion=1.0.${{ github.run_number }},supportsES6=true,withSeparateModelsAndApi=true,useSingleRequestParameter=true
# Compile TypeScript to JS + .d.ts. Publishing source-only would break consumers.
- name: Build
working-directory: ./sdk
run: |
npm install
npm run build
# Publish. NPM_TOKEN must be a repository secret with publish rights.
- name: Publish to npm
working-directory: ./sdk
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Gotchas & Edge Cases
supportsES6=false silently breaks tree-shaking. The default emits ES5 with module.exports, which modern bundlers cannot tree-shake, so consumers ship the entire client even if they call one method. Always set supportsES6=true for a browser-bound SDK.
Publishing source instead of dist forces consumers to compile your SDK. If you skip npm run build and publish the raw .ts files, every project that installs the package has to transpile it. Confirm package.json has "main": "dist/index.js" and "types": "dist/index.d.ts", and that dist is included in files or not excluded by .npmignore.
operationId collisions produce duplicate method names. Two operations sharing an operationId generate the same TypeScript method, and the second overwrites the first. Lint the spec for unique operationId values before generating, or the SDK will silently drop a method.
Pin the generator, not just the specification. A generated client is a function of two inputs, and teams routinely version-control only one of them. Running an unpinned generator means the emitted code can change without any change to your contract — new method names after a major bump, a different date type, a reshaped error class — and the resulting consumer breakage is genuinely confusing to debug because nothing in the specification’s history explains it. Record the generator version alongside the contract version in the published package so that any output is reproducible from two recorded facts.
Regenerate into a clean directory. Generating on top of an existing output leaves orphaned files behind when an operation is removed or renamed: the old client file stays, still compiles, still exports a method, and consumers keep calling an endpoint that no longer exists. Deleting the output directory before each run makes the generated tree an exact function of the inputs, which is the property that makes it safe to treat as disposable.
Decide what the SDK does about errors before the first release. The generated client’s error handling is the part consumers interact with most and the part generators handle least consistently — some throw on non-2xx, some return a result object, some expose the raw response. Whatever your generator does by default, document it in the SDK README and keep it stable, because changing it later is a breaking change for every catch block your consumers have written.
Publish the client as a package, not as a copied folder. Vendoring generated code into each consuming application means every consumer regenerates independently, at different contract versions, with different options — and nobody can answer which version of the API a given application is talking to. Publishing to a registry makes the client one artifact with one version, which is what lets a spec change reach consumers as a dependency update they can see and schedule.
Give the generated package a thin hand-written wrapper only if you truly need one. A wrapper is where teams put retries, auth refresh, and telemetry — genuinely useful things the generator will not produce. It is also a layer that must be maintained by hand forever and re-checked against every regeneration. Keep it minimal, keep it in its own file, and push anything expressible in the specification or the generator config back into those instead.
FAQ
Which OpenAPI Generator should I use for a TypeScript Axios client?
Use the typescript-axios generator, which produces a fetch-free client built on the axios HTTP library with full TypeScript types. It is the right choice when your app already uses axios or you want interceptors for auth and retries.
How do I set the npm package name when generating?
Pass npmName through --additional-properties, for example --additional-properties=npmName=@acme/api-client. The generator writes a package.json with that name so the output is ready to build and publish.
Do I need to compile the generated client before publishing?
Yes. The generator emits TypeScript source plus a package.json with a build script, so run npm install and npm run build to produce the .js and .d.ts files before npm publish. Publishing the raw .ts files would force every consumer to compile your SDK.
One habit ties all of this together: regenerate on every contract change, in CI, and compile the result. It is the cheapest possible check and it catches the whole class of problems where a specification edit is valid OpenAPI and produces code that will not build. Discovering that on a branch costs minutes; discovering it during a release costs the release.
Related
- OpenAPI Generator — parent guide to generators, config, and CI patterns
- Customizing OpenAPI Generator Templates
- Handling oneOf Discriminators in Generated SDKs
- SDK Generation & Changelog Automation
Keep the generated package boring. Its value is that it is an exact, reproducible function of two pinned inputs, and every hand-added convenience erodes that property a little.