Cookbook
Generate an API Client
Serve your API's OpenAPI spec from Zinc and generate a typed Go client for it with oapi-codegen.
This recipe serves an OpenAPI spec from a small Zinc API, generates a Go client from it with oapi-codegen, and calls the API through that client. Use it when another Go service, a CLI or a test suite talks to your API: the client’s types come from your handlers, so they can’t drift from them.
Run it
Section titled “Run it”The server:
mkdir pets-server && cd pets-servergo mod init example.com/pets-servergo get github.com/0mjs/zincSave the program below as main.go and run go run .. It listens on port 8080.
The program
Section titled “The program”package main
import ( "log" "net/http" "sync"
"github.com/0mjs/zinc")
type Pet struct { ID int64 `json:"id"` Name string `json:"name"` Kind string `json:"kind"`}
type CreatePet struct { Name string `json:"name" validate:"required,max=40"` Kind string `json:"kind" validate:"required,oneof=cat dog"`}
type PetID struct { ID int64 `path:"id"`}
var ( mu sync.Mutex pets = map[int64]Pet{})
func main() { app := zinc.New()
app.Post("/pets", zinc.Typed(func(c *zinc.Context, in CreatePet) (Pet, error) { mu.Lock() defer mu.Unlock() p := Pet{ID: int64(len(pets) + 1), Name: in.Name, Kind: in.Kind} pets[p.ID] = p return p, nil })).Status(http.StatusCreated).Name("createPet")
app.Get("/pets/{id}", zinc.Typed(func(c *zinc.Context, in PetID) (Pet, error) { mu.Lock() defer mu.Unlock() p, ok := pets[in.ID] if !ok { return Pet{}, zinc.NotFound("no such pet") } return p, nil })).Name("getPet").Errors(http.StatusNotFound)
app.OpenAPI("/openapi.json", zinc.OpenAPIConfig{Title: "Pets", Version: "1.0.0"})
log.Fatal(app.Listen(":8080"))}Try it
Section titled “Try it”In a second terminal, create a separate module for the client, with oapi-codegen as a tool:
mkdir pets-client && cd pets-clientgo mod init petappgo get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.8.0curl -s localhost:8080/openapi.json -o openapi.jsonTell oapi-codegen what to generate:
package: petclientgenerate: models: true client: trueoutput: petclient/petclient.gen.gogo tool oapi-codegen -config petclient/cfg.yaml openapi.jsongo mod tidyThe generated types match the handlers:
type CreatePet struct { Kind CreatePetKind `json:"kind"` // cat or dog, from oneof Name string `json:"name"`}
type Pet struct { Id int64 `json:"id"` Kind string `json:"kind"` Name string `json:"name"`}Call the API through the client:
package main
import ( "context" "fmt" "log"
"petapp/petclient")
func main() { client, err := petclient.NewClientWithResponses("http://localhost:8080") if err != nil { log.Fatal(err) } ctx := context.Background()
created, err := client.CreatePetWithResponse(ctx, petclient.CreatePetJSONRequestBody{Name: "Rex", Kind: petclient.Dog}) if err != nil { log.Fatal(err) } fmt.Println(created.StatusCode(), created.JSON201.Id, created.JSON201.Name)
got, err := client.GetPetWithResponse(ctx, created.JSON201.Id) if err != nil { log.Fatal(err) } fmt.Println(got.StatusCode(), got.JSON200.Name, got.JSON200.Kind)
missing, err := client.GetPetWithResponse(ctx, 99) if err != nil { log.Fatal(err) } fmt.Println(missing.StatusCode(), missing.JSON404.Error.Message)}go run .# 201 1 Rex# 200 Rex dog# 404 no such petHow it works
Section titled “How it works”zinc.Typedhandlers describe their input and output types, andapp.OpenAPIserves the spec built from them at/openapi.json..Name("createPet")sets the operation ID, which oapi-codegen turns into the methodCreatePetWithResponse. Unnamed routes get a name from their method and path instead.- Zinc checks the
validaterules itself, and the spec claims them:validate:"required"makes a request field required, so the client’s field is astring, not a*string. Response fields are always required unless they’reomitempty:encoding/jsonalways sends them. oneof=cat dogbecomes an enum, and oapi-codegen generates constants for it, such aspetclient.Dog..Errors(http.StatusNotFound)documents the 404 with Zinc’s error body, soJSON404.Error.Messageis typed.
Before production
Section titled “Before production”- Generate from a file you keep in the repository rather than from a running server, and regenerate when the API changes. Write the spec to a file from a test to keep it up to date.
- Pin oapi-codegen’s version, as
go get -toolabove does, so regenerating doesn’t change code you didn’t touch. - For clearer validation errors in the 422 response, use the adapter from Validate Input.
See also
Section titled “See also”- OpenAPI: everything the spec describes, and how to add to it.
- Typed Handlers: handlers whose types describe themselves.
- API Docs: a browsable page for the same spec.