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.
bun add @povio/openapi-codegen-clibunx openapi-codegen generate --input http://localhost:3001/docs-jsonThe 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.
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 generateThe 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 --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 --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)# 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 --verboseThe 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.
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;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>;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>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.
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,
};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.
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"],
},
);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[];
}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[];
}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;
}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.