typed-client

command
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

typed-client

A minimal end-to-end example: feed an OpenAPI spec to okapi-gen-typed, get a statically typed *Client with one method per operationId, and round-trip every operation against an in-process HTTP server.

This is the "I want to see okapi typed gen working" example — useful when you're new to the static gen, when you're wiring it up in your own service, or when you want to see what the generated output looks like for a tiny but realistic spec.

The dynamic-dispatch counterpart (no codegen, runtime spec parsing) is in ../client-test. They use the same spec so you can diff the call shapes side-by-side.

What's in here

File Purpose
openapi.yaml Tiny five-endpoint Items API. Covers query, path, and header params, required body fields, and a 422 problem response. Same spec as client-test/.
client/ Generator outputtypes.gen.go (typed structs) + client.gen.go (typed *Client with one method per operation). Committed; regenerate via go generate.
server.go A canned httptest server implementing the spec.
main.go The runner. Drives Healthz, ListItems, CreateItem, GetItem, DeleteItem, and a 422 negative path through the typed client.

Run it

go run ./examples/typed-client/

Expected output:

[1] healthz: status="ok"
[2] listItems: 1 item(s), first="Widget"
[3] createItem: id=2 name="Sprocket"
[4] getItem(2): name="Widget" created_at="2026-01-01T00:00:00Z"
[5] deleteItem(2): 204 No Content
[6] empty name → APIError(422) {"detail":"name is required","status":422,"title":"Unprocessable Entity"}

All six interactions succeeded.

Regenerate after a spec change

go generate ./examples/typed-client/

That re-invokes okapi-gen-typed against ./openapi.yaml and overwrites the two .gen.go files in client/. Commit the diff alongside the spec change — that's the API surface change your callers will see.

The //go:generate directive in main.go is the source of truth for the regen command:

//go:generate go run ../../cmd/okapi-gen-typed/ --source file://./openapi.yaml --package client --out ./client

In a downstream consumer (not in this repo) you'd typically pin the generator version with go run pkg@version:

//go:generate go run github.com/jathanism/okapi/cmd/okapi-gen-typed@v0.X.Y --source file://./openapi.yaml --package client --out ./client

That way the generator is fetched on demand and the version is explicit in the commit history.

What the generated surface looks like

For the spec in openapi.yaml, okapi-gen-typed produces:

// types.gen.go — body / response payloads only
type Item struct {
    Id        int64  `json:"id"`
    Name      string `json:"name"`
    CreatedAt string `json:"created_at"`
}

type CreateItemBody struct {
    Name string `json:"name"`
}

// (... Health, ItemList, Problem ...)

// client.gen.go — Client + per-op methods with positional args
type Client struct { /* BaseURL, *http.Client, DefaultHeaders */ }

func NewClient(baseURL string) *Client

func (c *Client) Healthz(ctx context.Context) (*Health, error)

// Optional query params are *T — pass nil to omit.
func (c *Client) ListItems(ctx context.Context, cursor *string, limit *int64) (*ItemList, error)

// Required header + body. Removing an arg fails to compile.
func (c *Client) CreateItem(ctx context.Context, idempotencyKey string, body CreateItemBody) (*Item, error)

// int64 path param.
func (c *Client) GetItem(ctx context.Context, id int64) (*Item, error)

// Multiple required headers — alphabetical order by Go name.
func (c *Client) DeleteItem(ctx context.Context, id int64, idempotencyKey string, ifMatch string) error

type APIError struct {
    StatusCode int
    Status     string
    Method     string
    URL        string
    Body       []byte
}
func (e *APIError) Error() string

The argument order is canonical and stable across regens: ctx → path params (URL-template order) → header params (alphabetical by Go name) → query params (alphabetical) → body. Required scalars are values; optional scalars are pointers.

Compile-time guarantees:

  • the path param id on GetItem is int64, not a generic stringly-typed request param
  • CreateItem won't compile without idempotencyKey — you can't silently zero-value it the way a struct field would let you
  • the Idempotency-Key casing is preserved on the wire
  • Healthz returns *Health, not any — IDE autocomplete works

Non-2xx server responses surface as *APIError; transport-level failures (DNS, connection refused, ctx cancellation) come back as plain wrapped errors. Use errors.As to discriminate:

var apiErr *client.APIError
if errors.As(err, &apiErr) {
    log.Printf("api %d: %s", apiErr.StatusCode, apiErr.Body)
}

Why use this over dynamic dispatch?

Use the typed gen (this example) when you're a production consumer of the API: the surface is SemVer-tracked, spec changes show up as compile-time errors at call sites, and there's no runtime spec parsing or reflection on the hot path.

Use dynamic dispatch when you're a tool that needs to adapt to arbitrary specs at runtime, or when you want a single binary that can drive whatever endpoint you point it at without a regen step.

The two approaches are not mutually exclusive — the typed gen imports nothing from okapi at runtime, so consumers can use both in the same binary if it makes sense.

Documentation

Overview

Command typed-client is the runnable example for okapi's statically typed client generator (`gen/typed`). It boots an in-process httptest server implementing the spec, instantiates the generated *Client, and round-trips every operation through it.

Run:

go run ./examples/typed-client/

Regenerate the client after a spec change:

go generate ./examples/typed-client/

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL