Skip to content

Documentation

swagger-typescript-api-es

Generate a fully typed TypeScript API client (fetch or axios) and data contracts from an OpenAPI 3.0 or Swagger 2.0 schema (JSON or YAML, from a URL or a local file).

Differences from the original

swagger-typescript-api-es is based on acacode/swagger-typescript-api and generates the same kind of client, with these differences:

  • ESM only, written in TypeScript. There is no CommonJS build.
  • Config file support. The CLI loads swagger-typescript-api.config.ts (or .js/.mjs/.cjs) from the project root. The file can export one config or an array of configs, and defaultConfig() types it for you.
  • The CLI flags are different. Flags are --u, --o, --n, --t, ... (for example -u <url> rather than upstream's -p <path>). Flags that take a value need one, so booleans are written as --modular true. See CLI.
  • oxfmt formats the output, not Prettier. Set formatter options with oxfmtOptrions (that spelling is the real option name) or with your project's .oxfmtrc.json.
  • output: false generates in memory and returns the files from generateApi without writing them.
  • The generated ContentType is a const object plus a type, not a TypeScript enum. Both generated HTTP clients also accept an injectHeaders callback.
  • There is no generate-templates CLI command. To copy the built-in templates, call generateTemplates() from code.

Performance

Generation time compared with the original swagger-typescript-api v13.13.0 (median, warm process, same documents and options, Node.js v22.22.2 on 4 cores):

Documentswagger-typescript-api-esswagger-typescript-api
Petstore (15 schemas, 11 operations)8 ms74 ms10× faster
300 schemas, 600 operations107 ms2.2 s20× faster
1500 schemas, 3000 operations494 ms11.3 s23× faster
1500 schemas, 3000 operations, modular426 ms14.9 s35× faster

A single cold run (new process, like the CLI) of the largest document takes about 938 ms instead of 13.1 s, with less than half the memory. All numbers, the method and the caveats are on the docs site's Benchmark page; run it yourself with npm run bench:compare.

Requirements

  • Node.js >= 20
  • ESM. The package is "type": "module", so import it with import, not require.
  • If you use httpClientType: 'axios', axios must be installed in the project that uses the generated client.

Install

Terminal
# npm
npm i -D swagger-typescript-api-es
# yarn
yarn add -D swagger-typescript-api-es
# pnpm
pnpm add -D swagger-typescript-api-es
# bun
bun add -d swagger-typescript-api-es

To run it once without installing:

Terminal
npx swagger-typescript-api-es@latest --help
# or: yarn dlx / pnpm dlx / bunx swagger-typescript-api-es@latest --help

Quick start

One-liner. This writes ./src/api/Api.ts:

Terminal
npx swagger-typescript-api-es@latest -u https://petstore.swagger.io/v2/swagger.json -o ./src/api

Config file. Create swagger-typescript-api.config.ts in the project root:

TypeScript
import { defaultConfig } from 'swagger-typescript-api-es';

export default defaultConfig({
  name: 'api-axios.ts',
  output: './src/apis/axios-gentype',
  url: 'http://localhost:5002/api-json',
  httpClientType: 'axios',
});

Then add a script to package.json and run it with npm run gen-api-types:

JSON
{
  "scripts": {
    "gen-api-types": "swagger-typescript-api-es"
  }
}

Config file

The CLI looks for a config file in the current working directory (with unreadconfig). The usual name is:

  • swagger-typescript-api.config.ts, .js, .mjs or .cjs

It also finds .swagger-typescript-apirc in any of these forms: bare, .json, .json5, .jsonc, .yaml, .yml, .toml, .ini, .js, .ts, .cjs, .mjs, or the same names under .config/. These rc files are checked before swagger-typescript-api.config.*. As a last resort, the CLI reads a "swagger-typescript-api" key in package.json. If none of these exist, the CLI needs --u (or --custom-config).

defaultConfig() only returns what you pass it. It exists so that the config is typed. The file can export a single config or an array of configs. Each config in an array is generated in turn:

TypeScript
import { defaultConfig } from 'swagger-typescript-api-es';

export default defaultConfig([
  {
    name: 'api-axios.ts',
    output: './src/apis/axios-gentype',
    url: 'http://localhost:5002/api-json',
    httpClientType: 'axios',
  },
  {
    name: 'api-fetch.ts',
    output: './src/apis/fetch-gentype',
    input: './openapi.yaml', // local file
    httpClientType: 'fetch',
    modular: true,
  },
]);

Precedence, from lowest to highest:

  1. The config file
  2. The file passed with --custom-config
  3. CLI flags

When the config file exports an array, the CLI flags and the --custom-config options are applied on top of every entry.

CLI

Terminal
swagger-typescript-api-es [options]
  • You can write flags in kebab-case or camelCase: --union-enums true and --unionEnums true are the same flag. Single-letter flags also work with one dash (-u, -o, -n, -t, -d, -r).
  • Boolean flags need a value. Write --modular true or --modular false. false, 0, no, off and an empty string count as false; anything else counts as true. A bare --modular fails with option '--modular <modular>' value is missing. --extract-enums is the only exception: it is a plain switch.
  • --module-name-index must be a number.
  • The exit code is 0 on success and 1 on any error. The error is printed as ❌ SWAGGER-TYPESCRIPT-API error: <message>.
  • -h, --help prints the help and -v, --version prints the version.
FlagOptionDefaultDescription
--u <url>url–URL or local path of the swagger schema
--o <output>output"./"Output directory
--n <name>name"Api.ts"Output file name (single-file mode)
--t <templates>templates–Path to a folder with custom templates
--d <bool>defaultResponseAsSuccessfalseTreat the default response as a success response too
--r <bool>generateResponsesfalseGenerate extra information about request responses, including typings for bad responses
--union-enums <bool>generateUnionEnumsfalseGenerate every enum as a union type (T1 | T2 | TN)
--add-readonly <bool>addReadonlyfalseMark readOnly: true schema properties as readonly
--route-types <bool>generateRouteTypesfalseGenerate type definitions for API routes
--noClient <bool>generateClient (negated)falseDo not generate the API class or HTTP client
--enum-names-as-values <bool>enumNamesAsValuesfalseUse the values in x-enumNames as enum values, not only as keys
--extract-request-params <bool>extractRequestParamsfalseExtract request params into a data contract, combining path and query params into one object
--extract-request-body <bool>extractRequestBodyfalseExtract request body types into data contracts
--extract-response-body <bool>extractResponseBodyfalseExtract response body types into data contracts
--extract-response-error <bool>extractResponseErrorfalseExtract response error types into data contracts
--extract-enumsextractEnumsfalseExtract inline enums into TypeScript enum declarations (plain switch, takes no value)
--modular <bool>modularfalseGenerate separate files for the HTTP client, data contracts and routes
--js <bool>toJSfalseGenerate a .js module with a .d.ts declaration file
--module-name-index <number>moduleNameIndex0Which path segment to group routes into modules by (GET:/fruits/getFruit with index 0 gives module fruits)
--module-name-first-tag <bool>moduleNameFirstTagfalseGroup routes into modules by their first tag
--disableStrictSSL <bool>disableStrictSSLfalseSkip TLS certificate checks when downloading the schema
--disableProxy <bool>disableProxyfalseIgnore HTTP(S)_PROXY env variables when downloading the schema
--httpClientType <fetch|axios>httpClientType"fetch"HTTP client used by the generated code
--unwrap-response-data <bool>unwrapResponseDatafalseResolve requests with the response data instead of the full response
--disable-throw-on-error <bool>disableThrowOnErrorfalse(fetch) Do not throw when response.ok is not true
--single-http-client <bool>singleHttpClientfalsePass an HttpClient instance to the Api constructor instead of extending it
--silent <bool>silentfalsePrint only errors
--default-response <type>defaultResponseType"void"Type to use for an empty response schema
--type-prefix <prefix>typePrefix""Prefix for data contract names
--type-suffix <suffix>typeSuffix""Suffix for data contract names
--clean-output <bool>cleanOutputfalseDelete the output folder's contents before generating. Can lose data
--api-class-name <name>apiClassName"Api"Name of the generated API class
--patch <bool>patchfalseFix small errors in the swagger source definition (Swagger 2.0 conversion)
--debug <bool>debugfalsePrint extra information about what the generator is doing
--another-array-type <bool>anotherArrayTypefalseWrite array types as Array<Type> (the default is Type[])
--sort-types <bool>sortTypesfalseSort types and their fields alphabetically
--sort-routes <bool>sortRoutesfalseSort routes alphabetically
--custom-config <path>––js/ts/json file (relative to the working directory) that exports an options object. Use it for options that have no flag, such as hooks.

Many options have no flag, for example hooks, requestOptions, oxfmtOptrions, input and extractingOptions. Set those in the config file or in --custom-config.

Options reference

These are all the fields of IOptions, which defaultConfig() and generateApi() accept. Relative paths resolve from process.cwd().

Input / output

OptionTypeDefaultDescription
urlstring–URL of the schema. A value that does not start with http(s):// is read as a local file path.
inputstring–Path to a local schema file (JSON or YAML). Used when the file exists.
specobject–A schema object already in memory. It takes precedence over input and url.
outputstring | false"./"Output directory. It is created recursively if missing, and generation fails if it points to an existing file. With false, nothing is written and the files are only returned from generateApi.
namestring"Api.ts"File name in single-file mode. The last extension is dropped and replaced by .ts (or by .js + .d.ts with toJS), so include the extension: my.api becomes my.ts.
cleanOutputbooleanfalseDelete everything in output before writing.
modularbooleanfalseWrite separate files. See Modular output.
toJSbooleanfalseEmit JavaScript (.js) plus declarations (.d.ts) instead of .ts.
templatesstring–Folder with custom .eta/.ejs templates. See Custom templates.
patchbooleanfalseLet swagger2openapi fix small errors while it converts Swagger 2.0.

Where the schema comes from, in order: spec, then input (if the file exists), then url.

Client

OptionTypeDefaultDescription
httpClientType'fetch' | 'axios''fetch'HTTP client of the generated HttpClient.
generateClientbooleantrueGenerate the HttpClient and Api classes. With false, only types are generated.
apiClassNamestring"Api"Name of the main API class (single-file mode).
singleHttpClientbooleanfalseApi receives an HttpClient in its constructor (new Api(http)) instead of extending it.
unwrapResponseDatabooleanfalseMethods resolve with the response body (Promise<T>) instead of HttpResponse<T, E> (fetch) or AxiosResponse<T> (axios).
disableThrowOnErrorbooleanfalsefetch client only: do not reject when response.ok is false, so the caller checks response.error.
generateResponsesbooleanfalseGenerate extra information about request responses, including typings for bad responses.
generateRouteTypesbooleanfalseGenerate a namespace of types per route (RequestParams, RequestQuery, RequestBody, RequestHeaders, ResponseBody).
defaultResponseTypestring"void"Type to use when a response has no schema.

Types and enums

OptionTypeDefaultDescription
generateUnionEnumsbooleanfalseGenerate enums as union types ('a' | 'b') instead of enum.
enumNamesAsValuesbooleanfalseUse x-enumNames as the enum values, not only as the keys.
addReadonlybooleanfalseMark properties that have readOnly: true in the schema as readonly.
anotherArrayTypebooleanfalseUse Array<T> instead of T[].
sortTypesbooleanfalseSort data contracts and their fields alphabetically.
typePrefixstring""Prefix for every generated type name.
typeSuffixstring""Suffix for every generated type name.
enumKeyPrefixstring""Prefix for enum keys.
enumKeySuffixstring""Suffix for enum keys.
fixInvalidTypeNamePrefixstring"Type"Prefix added to type names that are not valid identifiers (for example ones that start with a digit).
fixInvalidEnumKeyPrefixstring"Value"Prefix added to enum keys that are not valid identifiers.
defaultResponseAsSuccessbooleanfalseAlso use the default response as the success type.

Extraction

OptionTypeDefaultDescription
extractRequestParamsbooleanfalseMerge path and query params into one extracted type (e.g. ListPetsParams). The argument is optional when every field in it is optional.
extractRequestBodybooleanfalseExtract inline request bodies into named types (e.g. CreatePetPayload).
extractResponseBodybooleanfalseExtract inline success response bodies into named types (e.g. GetPetData).
extractResponseErrorbooleanfalseExtract inline error response bodies into named types (e.g. GetPetError).
extractEnumsbooleanfalseExtract inline enums into top-level enum declarations.
extractingOptionsobjectsee belowSuffixes for the names of extracted types. When a name is taken, the next suffix in the list is tried. Your arrays are deep-merged over the defaults by index.

<a id="extractingoptions-defaults"></a>Defaults of extractingOptions:

TypeScript
{
  requestBodySuffix: ['Payload', 'Body', 'Input'],
  requestParamsSuffix: ['Params'],
  responseBodySuffix: ['Data', 'Result', 'Output'],
  responseErrorSuffix: ['Error', 'Fail', 'Fails', 'ErrorData', 'HttpError', 'BadResponse'],
}

A request body is optional in the generated method unless the schema marks it required: true.

Routes and modules

OptionTypeDefaultDescription
moduleNameIndexnumber0Path segment used to group routes: /pets/{id} with index 0 gives module pets, so the method is api.pets.getPet().
moduleNameFirstTagbooleanfalseGroup routes by their first tag (camelCased) instead of by path segment.
sortRoutesbooleanfalseSort routes alphabetically within each module.
sortRoutersboolean–Deprecated alias of sortRoutes, used only when sortRoutes is not set.

Routes whose path has no module segment (for example GET /) become methods directly on the Api class. In modular mode they go to Common.ts.

Output formatting

OptionTypeDefaultDescription
oxfmtOptrionsFormatConfig (from oxfmt)built-in defaultsoxfmt options for the generated code, applied last. The spelling is intentional and is part of the public API. parser is ignored.

Network (schema download)

OptionTypeDefaultDescription
requestOptionsRecord<string, any> & { timeout?: number }null (timeout 60000)Extra undici fetch options (headers, dispatcher, signal, ...) for downloading the schema from url. timeout is in ms, and 0 disables it.
authorizationTokenstring–Sent as-is in the Authorization header when downloading the schema (for example "Bearer xxx").
disableStrictSSLbooleanfalseAccept self-signed or invalid TLS certificates for https schema URLs.
disableProxybooleanfalseBy default the download goes through the proxy from HTTP_PROXY / HTTPS_PROXY (respecting NO_PROXY). true connects directly.

Logging

OptionTypeDefaultDescription
silentbooleanfalsePrint only errors. Errors are still printed when this is true.
debugbooleanfalseVerbose logs with stack traces of the generator's internal steps.

Advanced

OptionTypeDefaultDescription
hooksobjectno-opsLifecycle hooks. See Hooks.
extraTemplates{ name: string; path: string }[][]More templates rendered with the same data. Each one is written as its own file named name (the extension becomes .ts).
templateInfos{ name: string; fileName: string }[]built-in listMaps template names to template file names. It replaces the built-in list (see Custom templates).
constantsRecord<string, any>built-in constantsMerged over the built-in constants, which are exported as constants and are available in templates as config.constants.
codeGenConstructs(constructs) => Record<string, any>–Override the TypeScript code constructs (config.Ts: Keyword, ArrayType, UnionType, TypeField, ...).
primitiveTypeConstructs(constructs) => Record<string, any>–Override how schema types and formats map to TypeScript types (see the example below).

Example: map date-time strings to Date and int64 integers to bigint.

TypeScript
import { defaultConfig } from 'swagger-typescript-api-es';

export default defaultConfig({
  url: 'https://petstore.swagger.io/v2/swagger.json',
  output: './src/api',
  primitiveTypeConstructs: (constructs) => ({
    string: { 'date-time': 'Date' },
    integer: { int64: 'bigint', $default: 'number' },
  }),
  codeGenConstructs: (constructs) => ({
    Keyword: { Any: 'unknown' }, // use `unknown` instead of `any`
  }),
});

Programmatic API

TypeScript
import {
  constants,
  defaultConfig,
  generateApi,
  generateTemplates,
} from 'swagger-typescript-api-es';

generateApi(options)

It returns a Promise of:

FieldTypeDescription
files{ fileName: string; fileExtension: string; fileContent: string }[]The generated, formatted files. They are returned even when output: false. The "generated file" header is added only on disk.
configurationobjectThe data passed to the templates (apiConfig, config, modelTypes, routes, utils, ...).
getTemplate({ fileName?, name?, path? }) => stringResolve a template's source.
renderTemplate(template: string, data: object, options?) => stringRender an Eta template string.
createFile({ path, fileName, content, withPrefix? }) => voidWrite a file (relative to process.cwd()).
formatTSContent(code: string) => Promise<string>Format code with the same oxfmt options.

If you pass an array of options, it runs them one after another and resolves with an array of results.

TypeScript
import { generateApi } from 'swagger-typescript-api-es';

// `output: false`: nothing is written to disk, the files are only returned
const { files } = await generateApi({
  url: 'https://petstore.swagger.io/v2/swagger.json',
  output: false,
  httpClientType: 'fetch',
  requestOptions: { timeout: 30_000 },
});

for (const { fileName, fileExtension, fileContent } of files) {
  console.log(`${fileName}${fileExtension}`, fileContent.length);
}

// an array of options gives an array of results
const results = await generateApi([
  { input: './specs/auth.yaml', output: './src/api/auth' },
  { input: './specs/billing.json', output: './src/api/billing', modular: true },
]);
console.log(results.length); // 2

generateApi rejects on errors: the schema is missing or invalid, the download fails, output is an existing file, and so on.

All public types are exported from the package entry: IOptions, Hooks, GenerateApiOutput, GeneratedFile, OpenAPIDocument, SchemaObject, ParsedRoute, ...

TypeScript
import type { GenerateApiOutput, IOptions } from 'swagger-typescript-api-es';

To pass a schema object you already have, use spec (typed as OpenAPIDocument, OpenAPI 3.x or Swagger 2.0). The object is never modified by the generator, and it is not even copied unless hooks, custom schema parsers, templates or type constructs are used:

TypeScript
import { readFile } from 'node:fs/promises';

import { generateApi } from 'swagger-typescript-api-es';

const spec = JSON.parse(await readFile('./openapi.json', 'utf8'));
await generateApi({ spec, output: './src/api' });

generateTemplates(params)

This copies the built-in templates (.ejs) into a folder so you can customize them. See Custom templates.

defaultConfig(options)

Returns options unchanged. It exists to type the config file.

constants

The built-in constants (HTTP_CLIENT, SCHEMA_TYPES, OXC_FORMAT_OPTIONS, TEMPLATES_DIR, RESERVED_*_ARG_NAMES, ...).

Using the generated client

The examples below come from a single-file client with the default apiClassName: 'Api'. The method names come from operationId (camelCased). Without one, the name is built from the HTTP method and the path.

fetch (default)

TypeScript
import { Api, ContentType, HttpResponse, Pet } from './src/api/Api';

const api = new Api<string>({
  baseUrl: 'https://api.example.com', // default: first `servers[].url` of the schema
  baseApiParams: { headers: { 'X-App': 'web' } }, // defaults for every request (RequestInit)
  // called for routes with `security` (or requests with `secure: true`)
  securityWorker: (token) => (token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
  // optional: custom fetch (e.g. for SSR, retries, tests)
  customFetch: (...args) => fetch(...args),
});

api.setSecurityData('my-jwt-token');

try {
  // modules come from the path (`/pets/...` -> `api.pets`)
  const response = await api.pets.getPet(1);
  const pet: Pet = response.data;
  console.log(pet.name);
} catch (e) {
  // by default a non-2xx response rejects with the HttpResponse
  const response = e as HttpResponse<unknown, unknown>;
  console.error(response.status, response.error);
}

// per-request params (RequestInit + secure/type/format/baseUrl/cancelToken)
await api.pets.listPets({ limit: 10 }, { headers: { 'X-Trace': '1' } });

// cancellation
const request = api.pets.listPets({ limit: 10 }, { cancelToken: 'pets-list' });
api.abortRequest('pets-list');
await request.catch(() => {});

// raw request
await api.request<Pet>({ path: '/pets', method: 'POST', body: {}, type: ContentType.Json });
  • With disableThrowOnError: true, failed responses resolve instead of rejecting. Check response.ok and response.error yourself.
  • With unwrapResponseData: true, methods resolve with the body (Promise<Pet>). A failed request still rejects with the HttpResponse.
  • injectHeaders: (headers) => headers | Promise<headers> in the constructor config can rewrite the headers of every request.

axios

Generate with httpClientType: 'axios' (and install axios):

TypeScript
import axios from 'axios';

import { Api } from './src/api/Api';

const api = new Api<string>({
  baseURL: 'https://api.example.com', // any AxiosRequestConfig option is accepted
  timeout: 10_000,
  securityWorker: (token) => (token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
  secure: false, // call securityWorker for every request when true
  format: 'json', // default responseType
  // instance: axios.create({ ... }), // or reuse your own instance (interceptors, ...)
});

api.setSecurityData('my-jwt-token');

try {
  const { data: pet } = await api.pets.getPet(1); // AxiosResponse<Pet>
  console.log(pet.name);
} catch (e) {
  if (axios.isAxiosError(e)) {
    console.error(e.response?.status, e.response?.data);
  }
}

// the underlying axios instance
api.instance.interceptors.request.use((config) => config);

With unwrapResponseData: true, methods resolve with response.data directly. Cancel requests with axios' own signal option: api.pets.getPet(1, { signal: controller.signal }).

singleHttpClient

TypeScript
import { Api, HttpClient } from './src/api/Api';

const http = new HttpClient({ baseUrl: 'https://api.example.com' });
const api = new Api(http);

Modular output

With modular: true, output gets several files instead of one (name is ignored):

Text
src/api/
├── data-contracts.ts   # all types (data contracts)
├── http-client.ts      # HttpClient, ContentType, RequestParams, ... (when generateClient)
├── Pets.ts             # class Pets extends HttpClient, one file per module
├── Store.ts
├── Common.ts           # routes outside any module, e.g. `GET /`
├── PetsRoute.ts        # namespace Pets { ... } route types (generateRouteTypes: true)
├── StoreRoute.ts
└── CommonRoute.ts      # route types of out-of-module routes (generateRouteTypes: true)
  • Module files are named PascalCase(moduleName).ts, and each exports a class of the same name. If that name clashes with a data contract, the class is named <Module>Api instead.
  • Route type files are <Module>Route.ts. They export namespace <Module>, or namespace <Module>Route on a clash.
  • With generateClient: false, only data-contracts.ts (and the route types) are written.
  • With toJS: true, every file becomes .js + .d.ts.
TypeScript
import { Pets } from './src/api/Pets';
import { Store } from './src/api/Store';

const config = { baseUrl: 'https://api.example.com' };
const pets = new Pets(config);
const store = new Store(config);

const { data } = await pets.listPets();

In single-file mode, generateRouteTypes: true adds the route namespaces to the same file.

Custom templates

Templates use Eta v2 syntax (<%~ raw %>, <%= escaped %>, <% code %>, data in it). Files may end in .eta or .ejs.

1. Copy the built-in templates:

TypeScript
import { generateTemplates } from 'swagger-typescript-api-es';

await generateTemplates({
  output: './api-templates', // omit to only get the files back
  httpClientType: 'axios', // which http-client.ejs to copy (default: 'fetch')
  modular: false, // copy the modular or default api templates
  rewrite: false, // overwrite templates that already exist in `output`
  cleanOutput: false, // empty `output` first
  silent: false,
});
// resolves with { files: { name, content }[], configuration, createFile }

The copied files are flattened into one folder. Their @base/, @default/ and @modular/ include paths are rewritten to ./.

2. Edit the files you need, and delete the rest. A template missing from your folder falls back to the built-in one.

3. Point the generator at the folder: templates: './api-templates' or --t ./api-templates.

Name (templateInfos)FileOutput
apiapi.ejsThe Api class (default) or a module class (modular)
dataContractsdata-contracts.ejsAll data contracts
dataContractJsDocdata-contract-jsdoc.ejsJSDoc of a data contract
interfaceDataContractinterface-data-contract.ejsAn interface contract
typeDataContracttype-data-contract.ejsA type contract
enumDataContractenum-data-contract.ejsAn enum contract
objectFieldJsDocobject-field-jsdoc.ejsJSDoc of an object field
httpClienthttp-client.ejsThe HttpClient class (includes http-clients/<type>)
routeTypesroute-types.ejsRoute type namespaces
routeNameroute-name.ejsReturns the method name of a route

The partials they include are procedure-call.ejs (one API method), route-docs.ejs (JSDoc of a method), route-type.ejs (one route namespace) and http-clients/{fetch,axios}-http-client.ejs.

How templates are found: your templates folder first, then templates/base, then templates/default or templates/modular. Inside a template, includeFile(path, data) accepts the prefixes @base/, @default/, @modular/, @original/ and @custom/. A path without a prefix is looked up in the custom folder, then in the built-in one.

What it contains: apiConfig (baseUrl, title, version, info, servers, tags, ...), config (all options, including config.Ts constructs and config.constants), modelTypes, routes ({ outOfModule, combined }), route (the current module, in modular api and routeTypes), hasSecurityRoutes, hasQueryRoutes, hasFormDataRoutes, generateResponses, fileName, translateToJavaScript and utils.

utils: Ts, formatDescription, escapeJSDocContent, internalCase, classNameCase, pascalCase, getInlineParseContent, getParseContent, getComponentByRef, parseSchema, checkAndAddNull, safeAddNullToType, isNeedToAddNull, inlineExtraFormatters, formatters, formatModelName, fmtToJSDocLine, NameResolver, require, and _, lodash-compatible helpers (from es-toolkit/compat): compact, merge, each, isEmpty, sortByProperty, noop, isObject, isString, isUndefined, map, uniq, size, replace, camelCase, lowerCase, values, join, get, upperCase, sortBy. require('./x') resolves from the templates folder. A package name resolves from your project first.

Extra files: extraTemplates: [{ name: 'endpoints.ts', path: './tpl/endpoints.ejs' }] renders each template with the same data and writes it next to the other files.

Hooks

Set hooks under hooks in the config file (or --custom-config) or in generateApi options. When a hook returns undefined, the original value is kept.

HookSignatureReturn value
onInit(config, codeGenProcess) => config | voidChanges to apply to the configuration (runs after the schema is loaded)
onPrepareConfig(templateData) => templateData | voidReplaces the data passed to the templates
onCreateComponent(component) => component | voidReplaces a #/components/* entry
onPreParseSchema(originalSchema, typeName, schemaType) => object | voidDeep-merged into the schema before it is parsed
onParseSchema(originalSchema, parsedSchema) => parsedSchema | voidReplaces the parsed schema
onCreateRoute(routeData) => routeData | false | voidReplaces the route. false skips it.
onCreateRouteName(routeNameInfo, rawRouteInfo) => routeNameInfo | voidReplaces { usage, original, duplicate }
onFormatRouteName(rawRouteInfo, templateRouteName) => string | voidNew method name
onFormatTypeName(typeName, rawTypeName, schemaType) => string | voidNew type name. schemaType is 'type-name' or 'enum-key'.
onCreateRequestParams(rawType) => schema | voidReplaces the schema of the request params object
onPreBuildRoutePath(routePath) => string | voidRewrites the raw path before its params are parsed
onBuildRoutePath({ originalRoute, route, pathParams, queryParams }) => same | voidReplaces the parsed path
onInsertPathParam(paramName, index, allParams, route) => string | voidExpression inserted into ${...} of the path template

All hooks are typed (Hooks is exported), so a .ts config gets autocompletion and type checking for their arguments and return values.

TypeScript
import { defaultConfig } from 'swagger-typescript-api-es';

export default defaultConfig({
  url: 'https://petstore.swagger.io/v2/swagger.json',
  output: './src/api',
  hooks: {
    // drop the "Dto" suffix from every type name
    onFormatTypeName: (typeName) => typeName.replace(/Dto$/, ''),
    // skip deprecated operations
    onCreateRoute: (routeData) => (routeData.raw.deprecated ? false : routeData),
    // name methods after the operationId, prefixed with the HTTP method
    onFormatRouteName: (routeInfo, templateRouteName) =>
      routeInfo.operationId ? `${routeInfo.method}_${routeInfo.operationId}` : templateRouteName,
  },
});

rawRouteInfo / routeData.raw contains the operation fields (operationId, method, route, moduleName, tags, summary, description, responses, requestBody, pathArgs, ...).

Formatting

Generated code is formatted with oxfmt. Options are merged in this order, and later ones win:

  1. Built-in defaults: singleQuote: true, jsxSingleQuote: true, printWidth: 100, trailingComma: 'es5', tabWidth: 2, semi: true, and grouped sortImports
  2. .oxfmtrc.json in the current working directory, if there is one (its $schema, ignorePatterns and overrides are ignored)
  3. oxfmtOptrions from your config. The spelling is intentional.
TypeScript
import { defaultConfig } from 'swagger-typescript-api-es';

export default defaultConfig({
  url: 'https://petstore.swagger.io/v2/swagger.json',
  oxfmtOptrions: { printWidth: 120, singleQuote: false, semi: false },
});

If formatting fails, a warning is printed and the code is written unformatted.

Troubleshooting / FAQ

  • The CLI exits with code 1. Every error is printed as ❌ SWAGGER-TYPESCRIPT-API error: ... and the process exits with 1, so a failed generation also fails CI.
  • option '--xxx <xxx>' value is missing. Boolean flags need a value: --modular true.
  • missing swagger schema. Pass --u <url> or create swagger-typescript-api.config.ts containing url, input or spec. Every entry of an array config needs one.
  • Local file or URL? url treats anything that does not start with http:// or https:// as a path relative to the working directory. input is always a local path. JSON and YAML both work.
  • Output path "..." is not a directory. output must be a directory. Set the file name with name / --n.
  • silent still prints something. silent hides logs, but errors are always printed.
  • Download timeouts. The schema download times out after 60 s. Change it with requestOptions: { timeout: 120_000 } (0 disables the timeout). This option has no CLI flag.
  • Self-signed certificates. Use disableStrictSSL: true (or --disableStrictSSL true).
  • Proxies. The download respects HTTP_PROXY / HTTPS_PROXY / NO_PROXY. Use disableProxy: true to connect directly, or pass your own undici dispatcher: requestOptions: { dispatcher: new ProxyAgent(proxyUrl) } (import { ProxyAgent } from 'undici'). The node-fetch style agent option is no longer supported.
  • Protected schema. Use authorizationToken: 'Bearer <token>' or requestOptions: { headers: { ... } }.
  • sortRouters is deprecated. Use sortRoutes.
  • Why is the body or params argument optional? A request body is optional unless the schema marks it required: true. With extractRequestParams, the params argument is optional when all its fields are optional.
  • require is not defined or ERR_REQUIRE_ESM. The package is ESM only. Use import (and "type": "module" or .mjs/.ts for your own scripts).

AI agent skill

Terminal
npx skills add hunghg255/swagger-typescript-api-es

Contributing

Terminal
git clone https://github.com/hunghg255/swagger-typescript-api-es
cd swagger-typescript-api-es
npm install
ScriptWhat it does
npm run buildBuild with unbuild and copy templates/ to dist/templates/
npm run devStub build (unbuild --stub) for fast local development
npm testRun the tests with Vitest (npm test -- --run runs them once)
npm run test:coverageRun the tests once with v8 coverage
npm run test:immutabilityRun the tests with deep-frozen input specs (the generator must not change them)
npm run lintLint with oxlint (lint:fix to fix)
npm run fmtFormat with oxfmt (fmt:check to check)
npm run typechecktsc --noEmit
npm startRun the playground play/test.ts
npm run benchBuild, then benchmark generation on small / large specs
npm run test:cliRun the built CLI (node dist/cli.mjs)
npm run test:cli1Run the CLI from source (tsx src/cli.ts)

Tests live in tests/:

  • tests/core: CLI, configuration, option parsing, schema resolving and download, generateApi and generateTemplates
  • tests/generation: generated output (snapshots, naming, parameters, responses, types)
  • tests/options: behaviour of individual options
  • tests/runtime: runs the generated fetch and axios clients against real requests
  • tests/fixtures and tests/helpers: shared schemas and helpers

To run a single file: npx vitest run tests/core/cli-start.test.ts.

Docs site and online playground

docs/ is a Next.js app with the documentation (rendered from this README) and a playground that generates clients in a serverless function (POST /api/generate), so you can try options, view the generated files and download them (one by one or as a .zip).

Terminal
npm run build          # the docs app uses the library from ../dist
cd docs && npm install
npm run dev            # http://localhost:3000

See docs/README.md for the API, its security limits and how to deploy it on Vercel.

CI runs lint, fmt:check, typecheck, the tests and the build on Node 20 and 22. Commit messages follow Conventional Commits (feat:, fix:, docs:, chore:, ...).

License

Licensed under the MIT License.

Based on swagger-typescript-api by acacode (MIT). Rewritten to TypeScript and ES modules by hunghg255.