Skip to content

Repository files navigation

OpenAPI code generation CLI

NOTE: This CLI tool is primarily designed for use within our organization. The generated code output aligns with our internal template.

NOTE: Version 1+ requires zod v4 and is not compatible with zod v3.

NOTE: The package includes supporting classes, types, components, and auth utilities. Zod is a required peer dependency. Axios, @tanstack/react-query, react, i18next, @casl/ability, @casl/react, and Vite are optional peers; install the ones used by your chosen features.

Axios is required when using the Axios transport (the default) or the /axios entry point. Projects using the native transport can omit Axios: generate with --restClient native and import NativeRestClient from @povio/openapi-codegen-cli/native. The root exports remain available for compatibility and support tree-shaking in consumer bundles. For direct runtime imports without bundling, use /native and /errors to avoid loading the Axios client. The /axios entry point is also available for Axios-specific imports. CLI-only usage and the /generator, /tiny, /vite, and /metro entry points do not require Axios.

Use this tool to generate code (Zod schemas, TypeScript types, API definitions, and React queries) from an OpenAPI v3 specification. API definitions use a REST client wrapper with either Axios or the native transport. React queries are generated in alignment with our code standards, without the need for explicit types.

The tool partially leverages code from openapi-zod-client repository.

Setup

bun add @povio/openapi-codegen-cli

Example

bunx openapi-codegen generate --input http://localhost:3001/docs-json

Configuration Files

The CLI supports TypeScript configuration files to simplify command execution and provide consistent settings with full type safety. Configuration files are automatically discovered in your project root.

Note: Command-line arguments always take precedence over configuration file values, allowing you to override specific settings when needed.

Quick Start

Create an openapi-codegen.config.ts file:

import type { OpenAPICodegenConfig } from "@povio/openapi-codegen-cli";

const config: OpenAPICodegenConfig = {
  input: "http://localhost:4000/docs-json/",
  output: "src/data",
};

export default config;

Then run without arguments:

bunx openapi-codegen generate

Configuration File Discovery

The CLI automatically searches for the TypeScript configuration file:

  • openapi-codegen.config.ts

You can also specify a custom configuration file:

bunx openapi-codegen generate --config my-config.ts

Options

Generate command (generates Zod schemas, API definitions and React queries)

  --config                            Path to TS config file (default: 'openapi-codegen.config.ts')
  --input                             Path/URL to OpenAPI JSON/YAML document
  --output                            Output directory path (default: 'output')
  --incremental                       Skip generation when OpenAPI and config are unchanged (default: true)
  --format                            Format the generated code using Oxfmt (default: true)
  --verbose                           Display detailed log messages during execution (default: false)

  --splitByTags                       Organize output into separate folders based on OpenAPI operation tags (default: true)
  --defaultTag                        (Requires `--splitByTags`) Default tag for shared code across multiple tags (default: 'Common')

  --includeTags                       Comma-separated list of tags to include in generation
  --excludeTags                       Comma-separated list of tags to exclude from generation
  --excludePathRegex                  Exclude operations whose paths match the given regular expression
  --excludeRedundantZodSchemas        Exclude any redundant Zod schemas (default: true)

  --tsNamespaces                      Wrap generated files in TypeScript namespaces (default: true)
  --importPath                        Module import style for generated files (default: 'ts'; options: 'ts' | 'relative' | 'absolute')
  --tsPath                            (Requires `--importPath` to be 'ts') Typescript import path (default: '@/data')
  --removeOperationPrefixEndingWith   Remove operation name prefixes that end with the specified string (default: 'Controller_')
  --extractEnums                      Extract enums into separate Zod schemas (default: true)
  --modelsInCommon                    Keep all schema declarations in defaultTag models and emit per-module proxy exports (default: false)
  --replaceOptionalWithNullish        Replace `.optional()` chains with `.nullish()` in generated Zod schemas (default: false)

  --restClient                        REST transport to generate: 'axios' or 'native' (default: 'axios')
  --axiosRequestConfig                Include transport request config parameters in query hooks (default: false)
  --infiniteQueries                   Generate infinite queries for paginated API endpoints (default: false)
  --mutationEffects                   Add mutation effects options to mutation hooks (default: true)
  --mutationScope                     Serialize mutations for the same path-param resource via TanStack scope.id (default: false).
                                      In config files also accepts { include: string[] } or { exclude: string[] } to opt specific
                                      operations in/out of scoping. Use "Tag/operationId" format for precision (e.g. "EmployeeSettings/update")
                                      or just "operationId" to match across all tags. Cannot specify both include and exclude.
  --mutationDefaultOnError            Use OpenApiQueryConfig.onError as the default onError for mutation hooks (default: false)
  --workspaceContext                  Comma-separated list of path/ACL params that generated hooks may resolve from OpenApiWorkspaceContext
  --inlineEndpoints                   Inline endpoint implementations into generated query files (default: false)
  --inlineEndpointsExcludeModules     Comma-separated modules/tags to keep as separate API files while inlineEndpoints=true
  --modelsOnly                        Generate only model files (default: false)
  --parseRequestParams                Add Zod parsing to API endpoints (default: true)

  --acl                               Generate ACL related files (default: true)
  --checkAcl                          Add ACL check to queries (default: true)

  --builderConfigs                    Generate configs for builders (default: false)

  --baseUrl                           (Requires `--restClientImportPath` to NOT be set) Base URL for the generated REST client; falls back to the OpenAPI spec if not provided

Check command (checks if OpenAPI spec is compliant)

  --config                            Path to TS config file (default: 'openapi-codegen.config.ts')
  --input                             Path/URL to OpenAPI/Swagger document as JSON/YAML
  --verbose                           Show log messages during execution

  --splitByTags                       Organize output into separate folders based on OpenAPI operation tags (default: true)
  --defaultTag                        (Requires `--splitByTags`) Default tag for shared code across multiple tags (default: 'Common')

  --includeTags                       Comma-separated list of tags to include in generation
  --excludeTags                       Comma-separated list of tags to exclude from generation
  --excludePathRegex                  Exclude operations whose paths match the given regular expression
  --excludeRedundantZodSchemas        Exclude any redundant Zod schemas (default: true)

Development

Test locally

# install dependencies
bun install

# run tests
bun run test

# run TypeScript sources directly with Bun
bun run start --help
bun run start generate --input ./test/petstore.yaml --verbose

# build new version
bun run build

# test build
bun run start --help
bun run start:dist generate --input ./test/petstore.yaml --verbose

Native Bun code generation

The Bun and Node.js CLIs and the Vite plugin automatically use the bundled Rust code-generation core when a compatible native binary is available. Generated files remain byte-for-byte compatible with the TypeScript implementation, which is retained as the fallback when the addon cannot be loaded.

Set OPENAPI_CODEGEN_NATIVE=0 to force the TypeScript path, or OPENAPI_CODEGEN_NATIVE=1 to require the native path and fail when its binary is unavailable.

Release packages include native binaries for Linux x64, macOS arm64, and Windows x64. Build a binary for the current platform with bun run build:native.

Run bun run test:parity after building the addon to compare every generated file across 64 layout combinations and representative values for every other renderer option. The matrix uses both test/petstore.yaml and test/configuration.yaml, exercises complete native and hybrid native generation, and loads generated models to check runtime references. The contradictory combination modelsInCommon: true with modelsInModules: true is rejected explicitly. Arbitrary strings and lists are covered by representative cases, not every possible value.

The separate Renderer parity workflow generates this matrix with JavaScript and native on Linux and macOS, then compares exact file lists and SHA-256 hashes. Artifacts include route reports identifying complete versus hybrid native generation. Runner integration tests cover input/output, stale-file cleanup, and unchanged files; incremental is currently retained as a compatibility option and does not change the writer's unchanged-file optimization.

Common Issues

App REST Client Interceptors

Select the fetch-based client without changing endpoint and query APIs:

import type { OpenAPICodegenConfig } from "@povio/openapi-codegen-cli";

export default {
  restClient: "native",
} satisfies OpenAPICodegenConfig;

Native mode imports common request/response contracts from @povio/openapi-codegen-cli/rest and the concrete NativeRestClient from @povio/openapi-codegen-cli/native. It uses fetch for normal requests and uploads, switching to XMLHttpRequest in browsers only when an upload progress callback is provided.

Native interceptors use the common transport interface:

import { NativeRestClient } from "@povio/openapi-codegen-cli/native";
import type { RestTransportInterceptor } from "@povio/openapi-codegen-cli/rest";

const authorizationInterceptor: RestTransportInterceptor = {
  onRequest(request) {
    request.headers.set("Authorization", `Bearer ${localStorage.getItem("accessToken")}`);
    return request;
  },
};

export const AppRestClient = new NativeRestClient({
  config: { baseURL: "https://api.example.com" },
  interceptors: [authorizationInterceptor],
});

Axios remains the default for backward compatibility. The existing Axios interceptor API remains available in Axios mode.

Native mode does not run the library ErrorHandler or create ApplicationException values. It throws HttpError for non-success HTTP responses and preserves Zod, network, cancellation, and timeout errors so applications can handle them directly in query callbacks, error boundaries, or their own normalization layer.

In order to add interceptors to the used REST client, you must create your own instance of a RestClient and pass your implemented interceptors into the constructor. Make sure to set restClientImportPath in your openapi generation configuration too.

import { RestInterceptor } from "@povio/openapi-codegen-cli/axios";

import { ACCESS_TOKEN_KEY } from "@/config/jwt.config";

export const AuthorizationHeaderInterceptor = new RestInterceptor((client) => {
  return client.interceptors.request.use(async (config) => {
    const accessToken = localStorage.getItem(ACCESS_TOKEN_KEY);
    if (accessToken != null) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }

    return config;
  });
});
import { RestClient } from "@povio/openapi-codegen-cli/axios";

import { AuthorizationHeaderInterceptor } from "@/clients/rest/interceptors/authorization-header.interceptor";
import { AppConfig } from "@/config/app.config";

export const AppRestClient = new RestClient({
  config: {
    baseURL: AppConfig.api.url,
  },
  interceptors: [AuthorizationHeaderInterceptor],
});
import type { OpenAPICodegenConfig } from "@povio/openapi-codegen-cli";

const config: OpenAPICodegenConfig = {
  restClientImportPath: "@/clients/app-rest-client",
  // ...
};

export default config;

Default mutation errors

Set mutationDefaultOnError: true in codegen config (or pass --mutationDefaultOnError) to let generated mutation hooks fall back to OpenApiQueryConfig.Provider when a mutation call does not define its own onError.

import { ErrorHandler } from "@povio/openapi-codegen-cli/errors";
import { OpenApiQueryConfig } from "@povio/openapi-codegen-cli/query";

<OpenApiQueryConfig.Provider
  onError={(error) => {
    errorToast({ text: ErrorHandler.getErrorMessage(error) });
  }}
>
  <App />
</OpenApiQueryConfig.Provider>;

Runtime response validation

Use OpenApiQueryConfig.Provider to allow generated GET query hooks to return invalid response data while still logging the response Zod error to the console. Non-GET requests still throw on invalid response data.

<OpenApiQueryConfig.Provider allowInvalidResponseData={import.meta.env.DEV}>
  <App />
</OpenApiQueryConfig.Provider>

OpenApiWorkspaceContext (Path + ACL defaults)

Set workspaceContext to a list of param names in codegen config (or pass --workspaceContext officeId,projectId) and wrap your app subtree with OpenApiWorkspaceContext.Provider if generated hooks frequently repeat workspace-scoped params.

import { OpenApiWorkspaceContext } from "@povio/openapi-codegen-cli/config";
// openapi-codegen.config.ts -> { workspaceContext: ["officeId", "projectId"] }

<OpenApiWorkspaceContext.Provider values={{ officeId: "office_123" }}>
  <MyWorkspacePages />
</OpenApiWorkspaceContext.Provider>;

Generated query/mutation hooks can then omit only those matching path/ACL params and resolve them from OpenApiWorkspaceContext. Params not listed in workspaceContext remain explicit and required.

Generation Modes

You can control whether API endpoint files are emitted, inlined into query files, or skipped entirely.

import type { OpenAPICodegenConfig } from "@povio/openapi-codegen-cli";

const config: OpenAPICodegenConfig = {
  // 1) Default mode: separate *.api.ts files are generated
  // 2) Inline mode: endpoint logic is generated inside *.queries.ts
  // and can be used without separate api files:
  // inlineEndpoints: true,
  // inlineEndpointsExcludeModules: ["Users", "Billing"],
  // 3) Models-only mode: generate only *.models.ts files
  // modelsOnly: true,
  // 4) Keep all model declarations in common.models and generate per-module model proxies
  // modelsInCommon: true,
};

Vite Plugin

You can run codegen directly from Vite config (without CLI config file):

import { defineConfig } from "vite";
import { openApiCodegen } from "@povio/openapi-codegen-cli/vite";

export default defineConfig({
  plugins: [
    openApiCodegen({
      input: "./openapi.yaml",
      output: "./src/data",
      inlineEndpoints: true,
      incremental: true,
      formatGeneratedFile: async ({ fileName, content }) => {
        void fileName;
        return content;
      },
    }),
  ],
});

The plugin runs on both vite serve and vite build, and watches local OpenAPI files in dev mode. If you provide formatGeneratedFile, the plugin formats each generated file in memory before comparing and writing it, which helps avoid unnecessary HMR when the formatted output is unchanged.

For Tiny projects that generate the OpenAPI JSON from ORPC before client codegen, use the wrapper plugin:

import { defineConfig } from "vite";
import { generateOpenApiFile as writeOpenApiFile, generateORPCOpenAPISpec } from "@povio/openapi-codegen-cli/tiny";
import { tinyOpenApiCodegen } from "@povio/openapi-codegen-cli/vite";
import { apiModules } from "../packages/fake-be/src/orpc/api/modules";
import { contract } from "../packages/fake-be/src/orpc/api/contract";
import { getOpenApiSchemaName } from "../packages/fake-be/src/orpc/spec";
import { userRoles } from "../packages/fake-be/src/roles";

const generateOpenApiFile = (options) =>
  writeOpenApiFile({
    ...options,
    generateOpenApiSpec: () =>
      generateORPCOpenAPISpec({
        contract,
        apiModules,
        userRoles,
        apiRoot: "../packages/fake-be/src/orpc/api",
        dbTablesRoot: "../packages/fake-be/src/db/tables",
        getOpenApiSchemaName,
      }),
  });

export default defineConfig({
  plugins: [
    tinyOpenApiCodegen(
      {
        input: "./openapi.generated.json",
        output: "./src/data",
        inlineEndpoints: true,
        incremental: true,
      },
      {
        generateOpenApiFile,
        watchFolders: ["../packages/fake-be/src/orpc", "../packages/fake-be/src/db"],
      },
    ),
  ],
});

When VITE_PUBLIC_API_MODE or EXPO_PUBLIC_API_MODE is real, the Tiny wrapper skips ORPC OpenAPI generation and behaves like openApiCodegen.

Metro Plugin

You can run codegen directly from React Native Metro config:

import { fileURLToPath } from "url";
import { getDefaultConfig } from "@react-native/metro-config";
import { withOpenApiCodegen } from "@povio/openapi-codegen-cli/metro";

const root = fileURLToPath(new URL("./", import.meta.url));
const config = getDefaultConfig(root);

export default withOpenApiCodegen(
  config,
  {
    input: "./openapi.yaml",
    output: "./src/data",
    inlineEndpoints: true,
    incremental: true,
    formatGeneratedFile: async ({ fileName, content }) => {
      void fileName;
      return content;
    },
  },
  { root },
);

The Metro wrapper runs generation when the config is loaded, waits for it before Metro transforms or serves the first request, and watches local OpenAPI files while the dev server is running. If you provide formatGeneratedFile, it behaves the same way as the Vite plugin.

For Tiny projects, use the Metro wrapper:

import { getDefaultConfig } from "@react-native/metro-config";
import { generateOpenApiFile as writeOpenApiFile, generateORPCOpenAPISpec } from "@povio/openapi-codegen-cli/tiny";
import { tinyOpenApiCodegenMetro } from "@povio/openapi-codegen-cli/metro";
import { apiModules } from "../../packages/fake-be/src/orpc/api/modules";
import { contract } from "../../packages/fake-be/src/orpc/api/contract";
import { getOpenApiSchemaName } from "../../packages/fake-be/src/orpc/spec";
import { userRoles } from "../../packages/fake-be/src/roles";

const root = __dirname;
const config = getDefaultConfig(root);
const generateOpenApiFile = (options) =>
  writeOpenApiFile({
    ...options,
    generateOpenApiSpec: () =>
      generateORPCOpenAPISpec({
        contract,
        apiModules,
        userRoles,
        apiRoot: "../../packages/fake-be/src/orpc/api",
        dbTablesRoot: "../../packages/fake-be/src/db/tables",
        getOpenApiSchemaName,
      }),
  });

export default tinyOpenApiCodegenMetro(
  config,
  {
    input: "./assets/openapi/main.json",
    output: "./utils/rest/openapi",
    inlineEndpoints: true,
    incremental: true,
  },
  {
    root,
    generateOpenApiFile,
    watchFolders: ["../../packages/fake-be/src/orpc", "../../packages/fake-be/src/db"],
  },
);

Enums

If you're using Enums in your backend DTOs with @Expose() and @IsEnum, they may still not appear correctly in the OpenAPI schema unless you also provide both enum and enumName to @ApiProperty.

enum Status {
  ACTIVE = "active",
  INACTIVE = "inactive",
}

export class ExampleDto {
  @ApiProperty({ enum: Status, enumName: "Status" })
  @Expose()
  @IsEnum(Status)
  status: Status;
}
enum Status {
  ACTIVE = "active",
  INACTIVE = "inactive",
}

export class ExampleDto {
  @ApiProperty({ enum: Status, enumName: "Status", isArray: true })
  @Expose()
  @IsEnum(Status, { each: true })
  @IsArray()
  status: Status[];
}

Nested objects

When using nested DTOs, ensure you explicitly specify the type using @ApiProperty({ type: NestedDto }):

export class NestedDto {
  @ApiProperty()
  @Expose()
  name: string;
}

export class ParentDto {
  @ApiProperty({ type: NestedDto })
  @Expose()
  @ValidateNested()
  @Type(() => NestedDto)
  @IsObject()
  nested: NestedDto;
}
export class NestedDto {
  @ApiProperty()
  @Expose()
  name: string;
}

export class ParentDto {
  @ApiProperty({ type: NestedDto, isArray: true })
  @Expose()
  @ValidateNested({ each: true })
  @Type(() => NestedDto)
  @IsArray()
  nestedList: NestedDto[];
}

JSON

When using JSON or Objects types, ensure you explicitly specify additional properties types as any otherwise FE ZOD will strip out everything: @ApiProperty({ additionalProperties: { type: 'any' } }):

export class JSONDto {
  @ApiProperty()
  @Expose()
  @IsObject()
  nested: NestedDto;
}
export class JSONDto {
  @ApiProperty({ additionalProperties: { type: "any" } })
  @Expose()
  @IsObject()
  nested: NestedDto;
}

Runtime import paths

Use package subpaths for runtime imports. Pure import type imports may use the package root.

Runtime exports Subpath
AuthContext, AuthGuard /auth
AbilityContext, useAclCheck, Can, createAclGuard /acl
ErrorHandler, SharedErrorHandler, ApplicationException, DomainErrorRegistry /errors
useMutationEffects, OpenApiQueryConfig /query
OpenApiRouter, OpenApiWorkspaceContext, useWorkspaceContext, translation configuration /config
ZodExtended /zod
HttpError, transport types /rest
NativeRestClient /native
RestClient, RestInterceptor (Axios transport) /axios

For example: import { AuthContext, AuthGuard } from "@povio/openapi-codegen-cli/auth". Custom application helpers should import error helpers from /errors, AbilityContext from /acl, and mutation effects from /query.

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages