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: Version 2+ includes supporting classes/types/components for the generated code as well as auth, therefore it has peerDependencies for @tanstack/react-query, axios, react and zod. @casl/ability and @casl/react are also required if you are generating ACL checks and/or using imports from "@povio/openapi-codegen-cli/acl"!
Use this tool to generate code (Zod schemas, TypeScript types, API definitions, and React queries) from an OpenAPI v3 specification. API definitions are generated to use a REST client wrapper that utilizes Axios. 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.
yarn add @povio/openapi-codegen-cliyarn 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 { 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:
yarn openapi-codegen generateThe CLI automatically searches for the TypeScript configuration file:
openapi-codegen.config.ts
You can also specify a custom configuration file:
yarn 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)
--axiosRequestConfig Include Axios 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)# prerequisites
corepack install
yarn
# run tests
yarn test
# run sources with tsx
yarn start --help
yarn start generate --input ./test/petstore.yaml --verbose
# build new version
yarn build
# test build
yarn start --help
yarn start:dist generate --input ./test/petstore.yaml --verboseIn 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";
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";
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, OpenApiQueryConfig } from "@povio/openapi-codegen-cli";
<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";
// 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.
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.
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;
}