Okapi is a standalone OpenAPI 3.x client library for Go. Give it an OpenAPI spec, and it gives you typed Go endpoints — no code generation step required at runtime (unless you want compile-time safety with go generate).
It parses your spec, builds a OpenApi struct with endpoint methods, validates request params and bodies against the schema, and makes the HTTP call. It also ships with a CLI generator that turns your spec into a nested command tree automatically.
- Spec-driven endpoints — parse any OpenAPI 3.x spec (local file,
file://,http(s)://) and call endpoints by name - Request validation — parameters are type-checked, required params are enforced, request bodies are validated against JSON Schema
- CLI generation —
go generateproduces a typedOpenApistruct with endpoint methods as fields; theclipackage builds a full command tree from the spec (using urfave/cli) - Flexible API client — bring your own HTTP client via the
OpenApiClientinterface; Okapi doesn't couple you to any specific HTTP library - Glamour help output — CLI help text is rendered with glamour markdown styling, including inline request body schemas
- Zero runtime dependencies on your app's context —
CliContextinterface lets you bridge Okapi into any CLI framework
package main
import (
"fmt"
"net/http"
"io"
"github.com/jathanism/okapi"
"github.com/jathanism/okapi/request"
)
func main() {
// Load an OpenAPI spec
api, err := (*openapi.OpenApi)(nil).NewFromSource("file://openapi.yaml")
if err != nil {
panic(err)
}
// Bind an API client
myClient := &MyHttpClient{}
api = api.WithClient(myClient)
// Call an endpoint
err = api.UsersList(
request.Param("limit", 10),
request.Param("offset", 0),
request.Result(&result),
)
}# Generate the OpenApi struct from a spec
OKAPI_OPENAPI_SOURCE=https://api.example.com/openapi.json go generate ./...
# Or use flags
go run ./gen/gen.go --source https://api.example.com/openapi.jsonThis produces openapi_gen.go with typed endpoint fields:
type OpenApi struct {
internal
AccountsChangePassword OpenApiEndpoint
OrganizationsBySlug OpenApiEndpoint
OrganizationsUsersCreate OpenApiEndpoint
OrganizationsUsersList OpenApiEndpoint
UsersCreate OpenApiEndpoint
UsersList OpenApiEndpoint
SchemasList OpenApiEndpoint
// ...
}okapi/
├── openapi.go # Core OpenApi struct — loads specs, builds endpoints, makes calls
├── openapi_gen.go # Generated OpenApi struct (do not edit — use go generate)
├── openapi_test.go # Integration tests (Ginkgo/Gomega)
├── error/ # Error types and helpers (OpenApiError, OpenApiValidationError)
├── request/ # Request building — params, body, headers, API client interface
├── spec/ # OpenAPI spec parsing, endpoint/param/body types, validation
├── cli/ # CLI generator — builds urfave/cli command tree from spec
├── gen/ # Code generator — reads spec, writes openapi_gen.go
├── internal/
│ ├── log/ # Structured logging (charmbracelet/log) with trace support
│ └── testutil/ # Test helpers
└── testdata/
└── openapi.yaml # Test fixture spec
The core package. OpenApi is the main type — it loads a spec, builds endpoint callables, and dispatches requests.
Key types:
OpenApi— spec-loaded API client with typed endpoint fieldsOpenApiEndpoint—func(options ...RequestOption) error— call an endpoint with optionsOpenApiSpec— alias forspec.OpenApiSpec
Key methods:
NewFromSource(source string)— load from file path, URL, or raw string (cached)NewFromBytes(source []byte)— load from raw bytes (not cached)WithClient(client OpenApiClient)— bind an HTTP client to all endpointsWith(options ...RequestOption)— clone with request options applied to all endpoints
Parses OpenAPI 3.x specs using pb33f/libopenapi. Handles parameter parsing, request body JSON Schema compilation, and URL building.
Functional options for building requests. Param(), Body(), Data(), Header(), Result(). The OpenApiClient interface is what you implement to bring your own HTTP client.
Generates a nested CLI command tree from an OpenAPI spec using urfave/cli. Commands mirror the spec's operationId structure (e.g., users list, organizations users create). Help text includes inline JSON body schemas rendered with glamour.
The CliContext interface bridges Okapi into your app's CLI runtime — implement it to provide stdin/stdout, host resolution, and JSON output formatting.
Code generator invoked via go generate. Reads a spec and writes openapi_gen.go with the typed struct. Configure with --source, --host, or the OKAPI_OPENAPI_SOURCE env var.
Custom error types with OpenApiError and OpenApiValidationError sentinels. Use Error(), Errorf(), and ErrorFrom() to create wrapped errors.
// From a file
api, err := (*openapi.OpenApi)(nil).NewFromSource("file:///path/to/openapi.yaml")
// From a URL
api, err := (*openapi.OpenApi)(nil).NewFromSource("https://api.example.com/openapi.json")
// From raw bytes
api, err := (*openapi.OpenApi)(nil).NewFromBytes([]byte(yamlContent))Implement the OpenApiClient interface to provide HTTP transport:
type OpenApiClient interface {
RequestJSON(method string, uri string, body io.Reader, result any, headers map[string][]string) (*http.Response, error)
}Then bind it:
api = api.WithClient(myClient)var result map[string]any
// Simple GET with query params
err := api.UsersList(
request.Param("limit", 10),
request.Result(&result),
)
// POST with body
err := api.UsersCreate(
request.Body(map[string]any{"email": "user@example.com", "password": "secret"}),
request.Result(&result),
)
// Path params, headers, and combined options
err := api.OrganizationsUsersList(
request.Param("organization_id", "org-123"),
request.Param("limit", 50),
request.Header("Authorization", "Bearer token"),
request.Result(&result),
)
// Endpoint chaining with .With()
customList := api.UsersList.With(
request.Param("limit", 100),
request.Header("Authorization", "Bearer token"),
)
err := customList(request.Result(&result))WithClient only binds the typed OpenApiEndpoint fields on the generated
OpenApi struct — it does not mutate the raw *spec.Endpoint values
returned by api.Endpoints(). If you fetch an endpoint from that map and try
to call it directly with openapi.CallEndpoint(ep, ...), you'll get:
No ApiClient available, did you forget to call OpenApi.WithClient()?
CallEndpoint is the low-level dispatcher used internally — it doesn't know
about the client you bound to your *OpenApi. You have two pragmatic options:
Note:
api.Endpoints()is keyed by the spec'soperationId(the raw name in the OpenAPI document, e.g.usersList). The matching field on the generated*OpenApistruct uses the CamelCased form returned by(*spec.Endpoint).MethodName()(e.g.UsersList). Use the raw name when looking up the endpoint, andMethodName()when looking up the field.
1. Pass the client through per-call (recommended — no reflect, works for any spec):
err := openapi.CallEndpoint(ep,
request.WithClient(myClient),
request.Result(&result),
)This is the simplest form and works whether or not you have a bound
*OpenApi. It's the right default for tooling that walks api.Endpoints()
or for tests that drive specs the generated struct doesn't match. See
examples/client-test for a runnable end-to-end
example that uses this pattern.
2. Reflective field lookup (when you specifically need the options bound to your *OpenApi):
Look up the generated struct field by MethodName() and invoke it. This
inherits everything bound via WithClient / With, so it's the form to
reach for when you've layered on auth headers, defaults, etc., and want
each dynamic call to pick those up:
api = api.WithClient(myClient).With(request.Header("Authorization", "Bearer "+token))
ep := api.Endpoints()["usersList"] // keyed by spec operationId
name := ep.MethodName() // CamelCased, e.g. "UsersList"
field := reflect.ValueOf(api).Elem().FieldByName(name)
fn := field.Interface().(openapi.OpenApiEndpoint)
err := fn(request.Result(&result))Okapi separates spec-declared parameters by location:
request.Param(name, value)— forpath,query, andcookieparametersrequest.Header(name, value)— forheaderparameters declared in the spec, and for ad-hoc HTTP headers (Authorization,Content-Type, etc.) that aren't in the spec at all
If a spec declares a parameter with in: header (e.g. Idempotency-Key), pass it through request.Header(...). Passing it through request.Param(...) will fail validation with a message pointing you at the right helper, and vice versa:
// Spec: Idempotency-Key is declared as `in: header`
// Correct:
err := api.UsersCreate(
request.Header("Idempotency-Key", "abc-123"),
request.Body(payload),
)
// Wrong — Validate returns:
// "Parameter Idempotency-Key is a header — pass it with
// request.Header(\"Idempotency-Key\", ...) instead of request.Param(...)"
err := api.UsersCreate(
request.Param("Idempotency-Key", "abc-123"),
request.Body(payload),
)Headers that aren't declared in the spec (auth tokens, tracing IDs, etc.) pass through to the API client untouched.
Okapi uses charmbracelet/log internally. Enable debug or trace output with the DEBUG env var:
DEBUG=1 # Debug level
DEBUG=trace # Trace level (verbose request/response details)- Go 1.26.1+
- An OpenAPI 3.x spec (JSON or YAML)
# Run all tests (Ginkgo + Gomega)
go test ./...
# Run a specific package
go test ./spec/...
go test ./request/...
# Generate from a spec
OKAPI_OPENAPI_SOURCE=file://testdata/openapi.yaml go generate ./...
# Or with flags
go run ./gen/gen.go --source file://testdata/openapi.yaml