Generate a production-ready Elixir API client library from an OpenAPI spec. Uses compile-time code generation — no runtime code generators, no build steps. The spec is the single source of truth.
Architecture
The generated library follows this structure:
{{APP_NAME}}/
mix.exs
.formatter.exs
openapi.json # OpenAPI spec (source of truth)
README.md
lib/
{{APP_NAME}}.ex # Public API — macro-generated endpoint functions
{{APP_NAME}}/
client.ex # HTTP client (Req-based, Bearer auth)
schemas.ex # Compile-time schema struct generation
test/
test_helper.exs
{{APP_NAME}}_test.exs # Unit tests (client, schemas)
{{APP_NAME}}/
integration_test.exs # Integration tests with mocked HTTP
Key design decisions:
- Compile-time generation from
openapi.jsonvia@external_resource— auto-recompiles on spec change - Req for HTTP — modern, composable, Finch-backed connection pooling
- Mimic for test mocking — mock
Reqcalls without behaviour boilerplate - Typed structs for every schema —
from_map/1recursively converts JSON to nested structs - No runtime dependencies on code generators — the generated library is self-contained
Prerequisites
- Elixir >= 1.17 installed
- User has an OpenAPI 3.x spec (JSON or YAML file, or a URL to fetch)
If the spec is YAML, convert to JSON during setup using Jason or yamerl, or ask the user to provide JSON.
Gather Input
Ask the user for:
- API name — human-readable name (e.g. "Stripe", "Twilio", "GoodVerify")
- App/package name — the Elixir app name as an atom, snake_case (e.g.
stripe_ex,twilio_ex). Suggest{{api_name_snake}}_exas default. - Module name — the top-level Elixir module (e.g.
StripeEx,TwilioEx). Derive from app name. - OpenAPI spec — path to a local file or URL to fetch. Accepts JSON or YAML.
- API base URL — default base URL (e.g.
https://api.stripe.com). Extract fromservers[0].urlin the spec if available. - Path prefix to strip — common API path prefix to strip when generating function names (e.g.
/api/v1/turns/api/v1/verify/emailintoverify_email). Detect from spec paths if they share a common prefix. - Auth style — how the API authenticates. Default: Bearer token in Authorization header. Also support: API key header (custom header name), no auth.
Derive these automatically:
{{MODULE_NAME}}— PascalCase module (e.g.GoodverifyEx){{APP_NAME}}— snake_case app name (e.g.goodverify_ex){{APP_ATOM}}— atom form (e.g.:goodverify_ex){{PATH_PREFIX}}— path prefix to strip (e.g./api/v1/){{BASE_URL}}— default base URL{{DEFAULT_BASE_URL}}— fallback for development (e.g.http://localhost:4000)
Generate
Read references/templates.md for all file templates.
1. Create project structure
mkdir -p {{APP_NAME}}/{lib/{{APP_NAME}},test/{{APP_NAME}},config}
2. Place the OpenAPI spec
Copy or download the spec to {{APP_NAME}}/openapi.json. If the user provided YAML, convert to JSON. If a URL, fetch it.
3. Write mix.exs
Use the mix.exs template. Set app: :{{APP_ATOM}}, version: "0.1.0". Include deps: req ~> 0.5, jason ~> 1.4, mimic ~> 2.0 (test only).
4. Write .formatter.exs
Standard formatter config covering {mix,.formatter}.exs and {config,lib,test}/**/*.{ex,exs}.
5. Write the main module — lib/{{APP_NAME}}.ex
This is the public API entry point. Uses compile-time code generation to iterate paths in the OpenAPI spec and generate one function per operation.
Function name derivation:
- Strip
{{PATH_PREFIX}}from the path - Replace
/with_ - Convert to atom
Function signatures:
- Operations with
requestBody→func_name(client, params)where params is a map - Operations without
requestBody→func_name(client)
Response handling:
- Extract
$reffromresponses.200.content.application/json.schema - Convert response body to the corresponding schema struct via
from_map/1 - Return
{:ok, struct}for 2xx,{:error, %{status, body}}for others,{:error, exception}for transport errors
Apply the main module template from references/templates.md.
6. Write the client module — lib/{{APP_NAME}}/client.ex
HTTP client struct with base_url, api_key (or equivalent auth field), and req_options.
Configuration priority: explicit opts > application config > defaults.
Auth header building depends on the auth style chosen:
- Bearer token:
{"authorization", "Bearer #{api_key}"} - API key header:
{{{HEADER_NAME}}, api_key} - No auth: empty headers
Apply the client template from references/templates.md.
7. Write the schemas module — lib/{{APP_NAME}}/schemas.ex
Compile-time schema generation from components.schemas in the spec. For each schema:
- Create a module
{{MODULE_NAME}}.Schemas.{{SchemaName}} - Define
defstructwith sorted field names fromproperties - Define
from_map/1that converts JSON maps to structs - Handle nested types:
$ref→ struct,anyOf/allOfwith$ref→ first struct found,arraywith$refitems → list of structs, otherwise passthrough
Apply the schemas template from references/templates.md.
8. Write test_helper.exs
Mimic.copy(Req)
ExUnit.start()
9. Write unit tests — test/{{APP_NAME}}_test.exs
Test client creation (defaults and explicit options), schema from_map/1 conversions for representative schemas, nil handling, unknown field handling, and that expected API functions are exported.
Select 2-3 representative schemas from the spec that exercise:
- Simple flat schemas
- Schemas with nested
$reffields - Schemas with array-of-ref fields
10. Write integration tests — test/{{APP_NAME}}/integration_test.exs
For each generated API function, write at least:
- One success case (200 response with full mock data, asserting struct types and field values)
- One error case (4xx or 5xx response)
Also test:
- Transport errors (connection refused, timeout)
- Auth header presence/absence
- Custom
req_optionspassthrough
Use the Mimic expect(Req, :request, fn opts -> ... end) pattern.
11. Write README.md
Follow this structure:
- Title and one-line description
- Installation (mix.exs dependency)
- Configuration (application config and runtime)
- Usage examples for each endpoint
- Error handling patterns
- Response types table
- Testing instructions
- License
12. Install dependencies and verify
cd {{APP_NAME}} && mix deps.get && mix compile && mix test
If compilation or tests fail, fix the issues before continuing.
13. Initialize git
cd {{APP_NAME}} && git init && git add -A && git commit -m "feat: initial {{API_NAME}} API client library"
Path Prefix Detection
To auto-detect {{PATH_PREFIX}}:
- Collect all paths from
spec["paths"] - Find the longest common prefix that ends with
/ - Suggest it to the user (e.g. "Detected common prefix
/api/v1/— strip it from function names?")
Handling Edge Cases
Path parameters
If paths contain {param} placeholders (e.g. /users/{id}), generate functions that accept the path parameter as an argument:
/users/{id}with GET →get_user(client, id)/users/{id}/postswith GET →list_user_posts(client, id)
Multiple response codes
If an operation defines multiple success responses (200, 201, 204), use the first one with a JSON schema. If 204 (no content), return {:ok, nil}.
Operations with operationId
If operationId is present, prefer it over path-derived names. Convert to snake_case atom.
Output
After generation, print:
Created {{APP_NAME}}/ — Elixir client for the {{API_NAME}} API
lib/{{APP_NAME}}.ex — Public API (N functions generated)
lib/{{APP_NAME}}/client.ex — HTTP client (Req-based)
lib/{{APP_NAME}}/schemas.ex — M schema structs generated
test/ — Unit + integration tests
Configuration:
config :{{APP_ATOM}},
base_url: "{{BASE_URL}}",
api_key: "your-api-key"
Quick start:
client = {{MODULE_NAME}}.client(api_key: "sk_...")
{:ok, result} = {{MODULE_NAME}}.{{EXAMPLE_FUNC}}(client)
Regenerate after spec changes:
Replace openapi.json and recompile — structs update automatically.