Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

apiwright

Generates typed API clients from an OpenAPI 3.1 spec:

  • Python: Pydantic v2 models over an async httpx client
  • TypeScript: types over a fetch client, with optional Zod schemas

3.0 specs are upconverted to 3.1 on load, so either version works as input.

Quick start

apiwright python -i openapi.yaml -o generated

That writes a complete, installable package. To generate both languages from a checked-in config instead:

apiwright init          # scaffolds apiwright.toml
apiwright generate      # generates every configured target

What gets written

Each output directory is a package, with one layout per language. Given package_name = "demo_client" and package_name = "demo-client":

python/                             typescript/
  pyproject.toml                      package.json
  demo_client/                        tsconfig.json
    __init__.py                       src/
    _client.py                          index.ts
    _auth.py                            client.ts
    _errors.py                          auth.ts
    _serde.py                           errors.ts
    py.typed                            models/
    models/                             api/
    api/                              tests/
  tests/                            .apiwright-manifest.json
  .apiwright-manifest.json

models/ holds one file per schema, api/ one file per tag. tests/ holds generated self-tests (round-trip and operation-signature checks) and can be turned off with emit_self_tests.

The package name is resolved in this order: the package_name config key, then --package-name, then the spec's info.title recased for the language, then client.

The manifest

Every run writes <output>/.apiwright-manifest.json, recording the hash of each file it owns. On the next run:

  • A file whose content still matches the manifest is rewritten or deleted freely
  • A file that has been hand-edited since generation is blocked, and the run reports it rather than overwriting it. --force overrides this
  • A file the generator no longer emits is deleted, and directories left empty by that deletion are pruned
  • Anything not in the manifest is left alone

.apiwright-manifest.json.lock sits alongside it and holds an advisory lock for the duration of a run, so two concurrent runs against one output cannot interleave. It is expected to persist between runs; the OS releases the lock if the process dies.

Generating into a directory that contains the input spec is refused, since that would feed the generator its own output.

Configuration

Config can live in any one of three hosts, all with the same keys:

  • apiwright.toml (tables at the top level)
  • pyproject.toml, under [tool.apiwright]
  • package.json, under an "apiwright" object

Discovery walks up from the working directory and stops at the first directory containing a config, or at a .git directory. Two config hosts in the same directory is an error rather than a precedence rule. -c/--config names one explicitly.

input = "openapi.yaml"

[python]
output = "generated/python"
package_name = "demo_client"

[typescript]
output = "generated/typescript"
package_name = "demo-client"
emit_zod = true

Keys

input is either a path string, relative to the config file, or a table:

input = { type = "file", path = "openapi.yaml" }

# Or fetch the spec from a command's stdout, for a spec that is generated
# rather than checked in:
input = { type = "command", command = ["python", "-m", "myapp.openapi"], cwd = ".", env = { ENVIRONMENT = "dev" } }

cwd is relative to the config file and defaults to it; env is merged onto the inherited environment.

[python] and [typescript] are both optional, and a target is generated only if its table is present.

Key Default Applies to
output required both
package_name required both
method_naming snake_case (Python), camel_case (TypeScript) both
emit_self_tests true both
post_emit_hook none both
emit_pyproject true Python
emit_package_json true TypeScript
emit_tsconfig true TypeScript
emit_zod false TypeScript

method_naming is one of snake_case, camel_case, or preserve.

post_emit_hook is a command run in the output directory after a successful write, for a formatter:

post_emit_hook = ["ruff", "format", "."]

A hook that reformats generated files does not cause the next run to report them as hand-edited. --no-post-emit-hook skips it.

Operation names

[operation_names] rewrites operationIds before they become method names. Rules run in order.

[operation_names]
rules = [
  { type = "strip_fastapi_suffix" },
  { type = "regex", match = "^Api_", replace = "", languages = ["python"] },
]

type = "regex" requires match and replace. type = "strip_fastapi_suffix" takes neither: FastAPI appends the path and method to every operationId, so get_notification_notifications__notification_key__get becomes get_notification. The suffix is reconstructed from the operation's own path and method and matched exactly, so an id from any other generator is left alone rather than guessed at.

languages limits a rule to python, typescript, or both; omitting it applies the rule everywhere.

These rules also name the models synthesised from inline request and response bodies, so a short bulk_create_works() returns a BulkCreateWorksResponse rather than a BulkCreateWorksWorksBulkPostResponse. Only rules with no languages restriction do this, since a model name is shared by both targets.

Commands

Command Purpose
apiwright generate Generate every target in the config
apiwright python Generate a Python client from a spec, no config needed
apiwright typescript Generate a TypeScript client from a spec, no config needed
apiwright init Scaffold a config. --into pyproject.toml or --into package.json writes into an existing file
apiwright check Parse, upconvert and normalize without emitting
apiwright print-ir Print the normalized IR as JSON, for debugging

The generating commands share --force, --dry-run, and --json. --dry-run reports what would change and creates nothing, not even the output directory. generate also takes -i/--input and -o/--output to override the config, and --no-post-emit-hook. --output is rejected when more than one target is enabled, since there would be no way to say which one it meant.

check and print-ir take either -c/--config or -i/--input. print-ir output is a function of the spec alone: naming config is applied later, on the way to the emitters, so what it prints is the IR before any renaming.

Development

The repository is a Rust workspace of four crates: apiwright-core (IR, normalisation, naming), apiwright-python, apiwright-typescript, and apiwright (the CLI). A nix flake pins the toolchain.

nix develop --command cargo test --workspace
nix develop --command cargo clippy --workspace --all-targets -- -D warnings
nix develop --command cargo fmt --all --check

The corpus gate

Unit tests check what the emitters produce. The corpus gate checks that the output actually works, by generating a client from each spec in corpus/ and then running it:

nix develop .#corpus --command cargo test -p apiwright --features corpus --test corpus

It is one test case per spec, Zod mode and language, so it parallelises and filters like any other cargo test: add a substring to run one of them.

The corpus feature gates the test target rather than the test skipping itself when its toolchains are missing. Without the feature the target is not compiled, so cargo test --workspace does not silently half-run it, and with the feature a missing python3, node, tsc, ruff or ty is a hard failure naming the shell to run under.

For each spec, in both Zod modes, it checks that the Python package imports and constructs, that the generated self-tests pass, that retries and backoff behave against a local server that fails on demand, and that ruff and ty are clean. Then the same for TypeScript under tsc and node --test.

Expectations specific to one spec live beside it, and run with the generated package importable:

  • corpus/<name>.checks.py runs against the Python package
  • corpus/<name>.checks.ts is copied into the package's tests/, so tsc checks it and then node runs it

A spec with no sidecar reports its expectations case as ignored rather than passing, so a missing expectation cannot be mistaken for coverage. Adding a spec to corpus/ is enough to have it generated and run; a sidecar is only needed if the spec is demonstrating something in particular.

An empty corpus is a failure rather than a pass, since a suite that checks nothing must not report success. The npm dependencies the generated TypeScript resolves through are pinned in scripts/corpus-npm/package-lock.json and installed with npm ci.

corpus/fastapi-shapes.yaml is a hand-written spec carrying the shapes real FastAPI output has and hand-written specs usually do not: mangled operationIds, inline request and response bodies, recursive schemas, and enums with defaults. It exists so that fidelity fixes are verified against something the generator will actually meet.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

apiwright-0.1.0rc3-py3-none-win_arm64.whl (1.6 MB view details)

Uploaded Python 3Windows ARM64

apiwright-0.1.0rc3-py3-none-win_amd64.whl (1.8 MB view details)

Uploaded Python 3Windows x86-64

apiwright-0.1.0rc3-py3-none-musllinux_1_2_x86_64.whl (1.9 MB view details)

Uploaded Python 3musllinux: musl 1.2+ x86-64

apiwright-0.1.0rc3-py3-none-musllinux_1_2_aarch64.whl (1.7 MB view details)

Uploaded Python 3musllinux: musl 1.2+ ARM64

apiwright-0.1.0rc3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

apiwright-0.1.0rc3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.7 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

apiwright-0.1.0rc3-py3-none-macosx_11_0_arm64.whl (1.6 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

apiwright-0.1.0rc3-py3-none-macosx_10_12_x86_64.whl (1.7 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file apiwright-0.1.0rc3-py3-none-win_arm64.whl.

File metadata

  • Download URL: apiwright-0.1.0rc3-py3-none-win_arm64.whl
  • Upload date:
  • Size: 1.6 MB
  • Tags: Python 3, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for apiwright-0.1.0rc3-py3-none-win_arm64.whl
Algorithm Hash digest
SHA256 fff3b2c031d083b34fe438f9aef96fccafb8b7297245ab508aca889ffe63f7c1
MD5 bf2e61f42dbf7dc15d96770f01800faa
BLAKE2b-256 027e3dfcf2b9d1ac64a337657195c93f31bdd7f73aad2645e70c14aed7f9001e

See more details on using hashes here.

File details

Details for the file apiwright-0.1.0rc3-py3-none-win_amd64.whl.

File metadata

  • Download URL: apiwright-0.1.0rc3-py3-none-win_amd64.whl
  • Upload date:
  • Size: 1.8 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for apiwright-0.1.0rc3-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 99657af348287d2dd6cdeb8ab696f92f4a7c1c5a9ce1bff8b673e1368852071d
MD5 ede307e441ab3d0737dd26550bf608c4
BLAKE2b-256 c10a521ebd11df462a19ae40b9f83570e4f02a21825609f2a66aa490dff7cb51

See more details on using hashes here.

File details

Details for the file apiwright-0.1.0rc3-py3-none-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for apiwright-0.1.0rc3-py3-none-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 cfd841b46b8287438716558a35c6272ad1531481ac61b302541a69aad9499ecc
MD5 c2685e4c7195b00859823589bee07575
BLAKE2b-256 f71a8336fb89489cf5eb5c76c0e7f7f3abfc905132ae80dfa4a1ee5c5d71055a

See more details on using hashes here.

File details

Details for the file apiwright-0.1.0rc3-py3-none-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for apiwright-0.1.0rc3-py3-none-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 ed0cb95eabdc82483ee8fa4254f8bcfe2107bf4cb315eef140a4089dd3f482c0
MD5 75d0e21d35d713ab6ec2ac5abb716abb
BLAKE2b-256 097ee9b11d4f9da38b7c5ac02f0301580b488f024d3347d22e04a9643fed7186

See more details on using hashes here.

File details

Details for the file apiwright-0.1.0rc3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for apiwright-0.1.0rc3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1c9a4c465998d650e68799c2e7feeeb3c8f633d413b0c10f1c1c88f30406b20f
MD5 c738b0dedab66493d09548c46271fe1a
BLAKE2b-256 fb3df7a62f6d04e90368787ae8d4ea908016d089c76a8880e828c942f8be57c0

See more details on using hashes here.

File details

Details for the file apiwright-0.1.0rc3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for apiwright-0.1.0rc3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 b6b8cf29ee90f95d2a4395162c034f144b966f74c59c2e20e36539fcebcd114b
MD5 c560c1e11ae0b6d6e58ceda2062e7a76
BLAKE2b-256 fe0ba8466c82159437d93cb1110d99776a4e3926d96e75b1204ca27216ebe772

See more details on using hashes here.

File details

Details for the file apiwright-0.1.0rc3-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for apiwright-0.1.0rc3-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1eeff871d9b38c72506480f3081f0a97ef06e751aac8281916379275b4fa6870
MD5 60818810be36930576a3811a18abefc2
BLAKE2b-256 872f248f5c92a2e40fe8697f574cb83d5bf54788ff0c22908f811e920ed19069

See more details on using hashes here.

File details

Details for the file apiwright-0.1.0rc3-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for apiwright-0.1.0rc3-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 d165127a0362747d9258ef6caa4461b28a25c75ac4591b554d35a725df918474
MD5 e81abbcbe93f00f4a2634a78b9d84ac4
BLAKE2b-256 0e5af92f3bbba58ee1a637a37e43e3cd2a843125e5be83b59e478a5a98e5e0ff

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0rc3 This release

8 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page