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.0rc4-py3-none-win_arm64.whl (1.6 MB view details)

Uploaded Python 3Windows ARM64

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

Uploaded Python 3Windows x86-64

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

Uploaded Python 3musllinux: musl 1.2+ x86-64

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

Uploaded Python 3musllinux: musl 1.2+ ARM64

apiwright-0.1.0rc4-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.0rc4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.7 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

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

Uploaded Python 3macOS 11.0+ ARM64

apiwright-0.1.0rc4-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.0rc4-py3-none-win_arm64.whl.

File metadata

  • Download URL: apiwright-0.1.0rc4-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.0rc4-py3-none-win_arm64.whl
Algorithm Hash digest
SHA256 0eb3a491f9dda29c2a420089d8beb2638267d9faeb570a9ebfbe31bbfac979ba
MD5 d3f9e82eeb64108a093f69365b1b6734
BLAKE2b-256 9e3e49aef19da7a559d3caf71e7a3a51668ad5b1802f620580301578b2dc9f20

See more details on using hashes here.

File details

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

File metadata

  • Download URL: apiwright-0.1.0rc4-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.0rc4-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 91199f4a4bb7655a95aa18062b6b2f38c869e6afc0b264befc06db3fd7db5a86
MD5 fcc70a86833e40dd3ab3f7637504f093
BLAKE2b-256 28f57cbce8699b94efc40033bc5d26c5b4fbf3661865d425bcea075e0b0c56ce

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for apiwright-0.1.0rc4-py3-none-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 61fbb41e85185b6854ef131037707786a5c71503026f8757388ac015e0531518
MD5 b82f476fe2e97e592a9d81d88155de6c
BLAKE2b-256 37d34805e0b458207a3d8aed188f1c1559c1beb76c60883ae24832e8bfb31ccd

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for apiwright-0.1.0rc4-py3-none-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 985404ec291f8658373ea0c7c30719aeba82d17a409e3b81a629485ff0d5cf57
MD5 d3b472be94b067f936fc73f611b3257e
BLAKE2b-256 39c09380de5c7c01c60bf28d24a192ecd970b0be96b70f6d88674d9c6510124a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for apiwright-0.1.0rc4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 df4b69226c556336ec297f0a3550b9bc73551210f26f7f1feb10006e3de931dd
MD5 9ca39c1e67083dabd2e4a453bd4127bf
BLAKE2b-256 58ee288179c179bddc32dd28b9f9f15b380e353a9cb4d21aa53b8b74a5141015

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for apiwright-0.1.0rc4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 a5de841aaa1e6bb3898de03407bb6298431dbee0c6d2a1da4aae0406fe465b84
MD5 d9da23e70f07dadbc9a5fe610f95ce42
BLAKE2b-256 48d80535b5bb3ef939dc2297b95e1a023a3885231eddb344663f477da14886fe

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for apiwright-0.1.0rc4-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9594a423372ceb21381557e1e9d8bd20210c3226d52c7926239a174113ec4a63
MD5 934db50078458a76a787a4b9d90fe098
BLAKE2b-256 8339e9d2b5913ad05aae5caba485651863f66f6a79cf0d395fe3be0aa9e93edb

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for apiwright-0.1.0rc4-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 dfe5156bdffca350c4087ba01ca1ee31239a170c4b287e0a2ab99c1bf0eaea94
MD5 2e669de914d7a4fc2fa03c3729126c54
BLAKE2b-256 a56f5b747cdce1d169fd67f66134d12c4b526741c7d3522243c657795e6e946a

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0rc4 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