humaclient

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Mar 23, 2026 License: MIT Imports: 12 Imported by: 0

README

Huma Client

A library to generate simple clients in Go for interacting with Huma-based API services. Features:

  • Generate a client SDK directly from a huma.API definition.
  • Convenient hook to add one-line client generation to your APIs.
  • Maintains Huma docs/validation tags for model re-use in other APIs.
  • Built-in support for paginated responses via Link headers with rel=next.
  • Support for Huma's autopatch PATCH operations via Patchable interface with MergePatch and JSONPatch types.
  • Conditional request helpers (WithIfMatch, WithIfNoneMatch) for ETag-based optimistic locking.
  • Pagination support for object-wrapped list responses with configurable items and next-page fields.
  • Server-Sent Events (SSE) streaming with typed event discrimination and iterator support.

These are not supported:

  • Union types oneOf, anyOf, and type arrays other than nullable types.
  • Non-JSON request bodies.

Example

Given a simple Huma API like this:

package main

import (
	"github.com/danielgtaylor/huma/v2"
)

type Thing struct {
	ID   string `json:"id" doc:"The unique identifier for the thing" minLength:"8" pattern="^[a-z0-9_-]+$"`
	Name string `json:"name" doc:"The name of the thing" minLength:"3"`
}

type GetThingResponse struct {
	Body Thing
}

func main() {
	mux := http.NewServeMux()

	api := humago.New(mux, huma.DefaultConfig("Example API", "1.0.0"))

	huma.Get(api, "/things/{thingID}", func(ctx context.Context, input *struct{
		ThingID string `path:"thingID"`
	}) (*GetThingResponse, error) {
		return &GetThingResponse{
			Body: Thing{
				ID:   input.ThingID,
				Name: "Example Thing",
			},
		}, nil
	})

	http.ListenAndServe(":8080", mux)
}

You can add support for generating a client SDK by including one line before starting the server (or initializing any dependencies like databases if you use them).

import (
	"github.com/danielgtaylor/humaclient"
)

// ...

humaclient.Register(api)
http.ListenAndServe(":8080", mux)

Now you can run your service like GENERATE_CLIENT=1 go run . This will generate a directory based on the API name with a Go client SDK for your API.

You can use it like this:

import (
	"fmt"

	"github.com/your-example/api/exampleapiclient"
)

func main() {
	client := exampleapiclient.New("http://localhost:8080")

	resp, thing, err := client.GetThingByID(ctx, "abc123")
	if err != nil {
		panic(err)
	}

	// Do something with the response and the retrieved thing
	fmt.Println(resp.Header)
	fmt.Println(thing)
}

Features

Custom Name

You can customize the package and interface name for your generated SDK.

import (
	"github.com/danielgtaylor/humaclient"
)

// ...

humaclient.RegisterWithOptions(api, humaclient.Options{
	PackageName: "custompkg",
	ClientName:  "CustomClient",
})
Custom Output Directory

You can customize the output directory where the generated SDK is created.

import (
	"github.com/danielgtaylor/humaclient"
)

// ...

humaclient.RegisterWithOptions(api, humaclient.Options{
	OutputDirectory: "./generated/clients/myapi",
})

If not specified, the generated SDK will be placed in a directory named after the package name in the current working directory.

Custom Client

You can provide a custom HTTP client to handle e.g. authentication using functionality built into the Go standard library:

type HeadersTransport struct {
	Transport http.RoundTripper
	Headers   map[string]string
}

func (t *HeadersTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	for key, value := range t.Headers {
		req.Header.Set(key, value)
	}
	return t.Transport.RoundTrip(req)
}

// ...

client := exampleapiclient.NewWithClient("http://localhost:8080", &http.Client{
	Transport: &HeadersTransport{
		Transport: http.DefaultTransport,
		Headers: map[string]string{
			"Authorization": "Bearer " + token,
		},
	},
})

The golang.org/x/oauth2 package is useful for this as well.

Request Bodies

Request bodies, when required, become a part of the generated client methods.

client.PutThingByID(ctx, "abc123", Thing{
	ID:   "abc123",
	Name: "Updated Thing",
})

Read on for optional request bodies.

Optional Parameters

You can set optional defined parameters as well as custom query params or headers when making outgoing requests.

// Custom header example
client.GetThingByID(ctx, "abc123", exampleapiclient.WithHeader("X-Custom-Header", "value"))

// Custom query param example
client.GetThingByID(ctx, "abc123", exampleapiclient.WithQuery("include", "related"))

// Passing an optional body
client.OptionalBodyExample(ctx, "abc123", exampleapiclient.WithBody(Thing{
	ID:   "abc123",
	Name: "Updated Thing",
}))

If optional parameters are defined by the API spec, then they can be used as functional options when making requests using a struct specific to that operation:

// Passing API-defined optional parameters
client.ListThings(ctx, exampleapiclient.WithOptions(exampleapiclient.ListThingsOptions{
	Cursor: "abc123",
	Limit: 100,
}))
Autopatch / JSON Merge Patch

Huma's autopatch feature automatically generates PATCH operations from GET + PUT pairs using application/merge-patch+json. The client generator detects these operations and generates a Patchable interface with two implementations:

The correct Content-Type header is set automatically based on which type you use.

// Merge Patch: send only the fields you want to change
client.PatchThingByID(ctx, "abc123", exampleapiclient.MergePatch{
	"name": "new name",
})

// JSON Patch: explicit list of operations
client.PatchThingByID(ctx, "abc123", exampleapiclient.JSONPatch{
	{Op: "replace", Path: "/name", Value: "new name"},
})

For optimistic locking with ETags, use the generated WithIfMatch helper:

// First, GET the resource and capture the ETag
resp, thing, _ := client.GetThingByID(ctx, "abc123")
etag := resp.Header.Get("ETag")

// Then PATCH with If-Match for safe concurrent updates
client.PatchThingByID(ctx, "abc123", exampleapiclient.MergePatch{
	"name": "updated name",
}, exampleapiclient.WithIfMatch(etag))

WithIfNoneMatch is also available for conditional requests.

The Patchable interface is public, so you can define your own typed patch structs for strong typing if desired:

type ThingPatch struct {
	Name string `json:"name,omitempty"`
}

func (p ThingPatch) PatchContentType() string {
	return "application/merge-patch+json"
}

// Use your typed struct just like MergePatch or JSONPatch
client.PatchThingByID(ctx, "abc123", ThingPatch{Name: "new name"})
Pagination

Pagination is supported via the standard Link header with a relationship like rel=next. If that header is documented in your API and the response returns a list of resources, then a method will be generated to provide an iterator that returns each item in the collection, transparently fetching the next request as needed until no pages remain.

// Example of using the pagination iterator
for item, err := range client.ListThingsPaginator(ctx) {
	if err != nil {
		fmt.Println("Error:", err)
		break
	}
	fmt.Println(item)
}
Object-Wrapped List Responses

Many APIs return list responses wrapped in an object with additional metadata rather than as a plain JSON array:

{
  "items": [{"id": "1", "name": "Thing 1"}, {"id": "2", "name": "Thing 2"}],
  "total": 42,
  "next": "https://api.example.com/things?cursor=abc123"
}

To support this, configure PaginationOptions when registering your API:

humaclient.RegisterWithOptions(api, humaclient.Options{
	Pagination: &humaclient.PaginationOptions{
		// Go struct field name containing the items array (required)
		ItemsField: "Items",
		// Go struct field path containing the next-page URL (optional)
		NextField:  "Next",
	},
})

The generated raw method returns the full wrapper struct, giving you access to all metadata:

resp, result, err := client.ListThings(ctx)
fmt.Println(result.Items) // the items
fmt.Println(result.Total) // additional metadata
fmt.Println(result.Next)  // next page URL

The paginator automatically unwraps items and handles pagination transparently:

for item, err := range client.ListThingsPaginator(ctx) {
	if err != nil {
		fmt.Println("Error:", err)
		break
	}
	fmt.Println(item)
}

Configuration options:

  • ItemsField (required): The Go struct field name of the array field (e.g. "Items", "Data", "Results"). Must be a root-level field.
  • NextField (optional): The Go struct field path for the next-page URL. Supports dot-separated paths for nested fields (e.g. "Next", "Meta.Next", "Pagination.NextURL"). When set, enables body-based pagination.

When both a Link header and a body next-page field are available, the Link header takes precedence. If Pagination is nil (the default), only array responses with Link headers are treated as paginated, preserving backward compatibility.

Server-Sent Events (SSE)

SSE endpoints registered with Huma's sse.Register() are automatically detected and generate two methods per operation:

  • A base method (e.g. WatchChat) that returns (*http.Response, error) with the body left open for manual consumption
  • A stream method (e.g. WatchChatStream) that returns iter.Seq2[SSEEvent, error] for ergonomic iteration

Event data is pre-unmarshaled based on the event type, so you can type-switch directly:

for event, err := range client.WatchChatStream(ctx) {
    if err != nil {
        log.Fatal(err)
    }
    switch data := event.Data.(type) {
    case exampleapiclient.ChatMessage:
        fmt.Printf("%s: %s\n", data.User, data.Message)
    case exampleapiclient.UserJoinedEvent:
        fmt.Printf("%s joined the chat\n", data.User)
    default:
        fmt.Println("Unknown event:", event.Type, event.Data)
    }
}

The SSEEvent struct provides access to all SSE fields:

type SSEEvent struct {
    Type  string // Event type (e.g. "userJoin"). Empty for default "message" events.
    ID    string // Optional event ID (string per SSE spec)
    Retry int    // Optional retry time in milliseconds
    Data  any    // Pre-unmarshaled data; type-assert based on event type
}

Context cancellation stops the stream iterator. For low-level control (e.g. using channels, select{}, or custom parsing), use the base method:

resp, err := client.WatchChat(ctx)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()
// Read resp.Body manually...

Sometimes a response may contain a link to a related resource. There are various mechanisms for accomplishing this, and the client generator is non-opinionated about how you generate and share such links in your response.

Once a link is received by the client, you can follow it to retrieve the related resource and return the appropriate Go type from the response.

// Get a link from a response in some way, e.g. a `Self` field or `Link` header
link := "..."

// Follow the link to fetch the related resource.
var related Thing
resp, err := client.Follow(ctx, link, &related)
if err != nil {
	panic(err)
}

You can also pass custom params when following links:

var related Thing
_, err := client.Follow(ctx, link, &related, exampleapiclient.WithQuery("some", "value"))
Model Reuse

Models in the generated SDK code contain the docs and validation in the original API models, meaning they can be re-used in another Huma API. When you have many microservices this may be be desirable for one service to collect information from others and expose it as a single endpoint or proxy.

For example, the generated code for the Thing above would look like this:

type Thing struct {
	ID   string `json:"id" doc:"The unique identifier for the thing" minLength:"8" pattern="^[a-z0-9_-]+$"`
	Name string `json:"name" doc:"The name of the thing" minLength:"3"`
}

The inclusion of the doc, minLength, and pattern validation fields is preserved so they can be re-used as a request or response object in another Huma API. All of the fields at https://huma.rocks/features/request-validation/ are supported.

Model Referencing

In some cases the models may come from a shared package, allowing for easier reuse across different services. The generated SDK code supports this by opting-in to the list of allowed packages that can be imported and used in the generated code.

humaclient.RegisterWithOptions(api, humaclient.Options{
	AllowedPackages: []string{"github.com/danielgtaylor/huma/v2"}
})

If, for example, an operation returns a huma.Schema response body, the generated code will now reference import github.com/danielgtaylor/huma/v2 and the generated operation will return a huma.Schema as well rather than redefining the Schema struct in the generated code.

Use this feature with care as you can unintentionally break clients by changing shared library code!

Development

CI/CD Pipeline

This project uses GitHub Actions for continuous integration and delivery. The pipeline runs on every push and pull request.

Running Tests Locally
# Run all tests
go test ./...

# Run tests with coverage
go test -v -race -coverprofile=coverage.out ./...

# Run benchmarks
go test -bench=. -benchmem ./...

# Test client generation
cd example
GENERATE_CLIENT=1 go run main.go
Contributing
  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes and add tests
  4. Ensure all tests pass and linting is clean
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

The CI pipeline will automatically run all tests and quality checks on your PR.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateClient

func GenerateClient(api huma.API) error

GenerateClient generates a client SDK for the given Huma API

func GenerateClientWithOptions

func GenerateClientWithOptions(api huma.API, opts Options) error

GenerateClientWithOptions generates a client SDK for the given Huma API with custom options

func LowerCamel

func LowerCamel(value string, transform ...casing.TransformFunc) string

LowerCamel returns a lowerCamelCase version of the input.

func Register

func Register(api huma.API)

Register adds a Huma API to be processed for client generation. This should be called after setting up your API but before starting the server.

func RegisterWithOptions

func RegisterWithOptions(api huma.API, opts Options)

RegisterWithOptions adds a Huma API to be processed for client generation with custom options. This should be called after setting up your API but before starting the server.

Types

type ClientTemplateData

type ClientTemplateData struct {
	PackageName         string
	ClientInterfaceName string
	ClientStructName    string
	Imports             []string
	ExternalImports     []string // External packages that are allowed to be referenced
	Schemas             []SchemaData
	Operations          []OperationData
	RequestOptionFields []OptionField
	HasRequestBodies    bool
	HasMergePatch       bool // Whether any operation supports both merge-patch+json and json-patch+json (autopatch)
	HasJSONPatchOp      bool // Whether JSONPatchOp already exists as a schema struct
	HasSSE              bool // Whether any operation uses Server-Sent Events
}

ClientTemplateData holds data for client code generation

type FieldData

type FieldData struct {
	Name     string
	Type     string
	JSONTag  string
	HumaTags string
}

FieldData represents a field in a struct

type OperationData

type OperationData struct {
	MethodName        string
	HTTPMethod        string
	Path              string
	PathParams        []ParamData
	HasRequestBody    bool
	RequestBodyType   string
	HasOptionalBody   bool
	HasResponseBody   bool
	ReturnType        string
	ZeroValue         string
	HasQueryParams    bool
	HasHeaderParams   bool
	QueryParams       []ParamData
	HeaderParams      []ParamData
	OptionsStructName string
	OptionsFields     []OptionField
	IsPaginated       bool
	ItemType          string
	IsMergePatch      bool               // Whether this operation has both merge-patch and json-patch media types (autopatch detection)
	ItemsField        string             // Go struct field name for items array in wrapped responses (e.g. "Items")
	NextField         string             // Go struct field path for next-page URL (e.g. "Next" or "Meta.Next")
	NextFieldNilCheck string             // Nil-check expression for nullable intermediate fields in NextField path
	ResponseType      string             // Wrapper struct type name for object-wrapped paginated responses
	IsSSE             bool               // Whether this operation returns text/event-stream (SSE)
	SSEEventTypes     []SSEEventTypeData // Event types for SSE operations
}

OperationData represents an API operation for code generation

type OptionField

type OptionField struct {
	Name     string
	Type     string
	JSONName string
	Tag      string
	In       string // "query" or "header"
}

OptionField represents an optional parameter field

type Options

type Options struct {
	PackageName     string             // Custom package name (default: generated from API title)
	ClientName      string             // Custom client interface name (default: generated from API title)
	AllowedPackages []string           // List of allowed Go packages that can be referenced instead of recreated
	OutputDirectory string             // Custom output directory (default: package name in current directory)
	Pagination      *PaginationOptions // Pagination options for object-wrapped list responses
}

Options provides customization options for client generation

type PaginationOptions added in v0.0.6

type PaginationOptions struct {
	// ItemsField is the Go struct field name in the response object that
	// contains the array of items (e.g. "Items", "Data", "Results").
	// Must be a root-level field.
	ItemsField string

	// NextField is the Go struct field path in the response object that
	// contains the URL for the next page. Supports dot-separated paths for
	// nested fields (e.g. "Next", "Meta.Next", "Pagination.NextURL").
	// Optional. When set, enables body-based pagination in addition to or
	// instead of Link header pagination.
	NextField string
}

PaginationOptions configures how wrapped list responses are paginated. When set on Options, the generator will detect object responses containing an array field and generate paginator methods that iterate through items.

type ParamData

type ParamData struct {
	Name             string
	GoName           string
	GoNameLowerCamel string
	Type             string
	Required         bool
}

ParamData represents a parameter

type SSEEventTypeData added in v0.1.0

type SSEEventTypeData struct {
	EventName string // SSE event name (e.g. "userCreate", "message")
	GoType    string // Go type name for the event data (e.g. "UserCreateEvent")
}

SSEEventTypeData represents a single event type in an SSE operation

type SchemaData

type SchemaData struct {
	Name         string
	StructName   string
	Fields       []FieldData
	IsExternal   bool   // Whether this schema comes from an external allowed package
	ExternalType string // Full external type name (e.g., "huma.Schema")
}

SchemaData represents an OpenAPI schema for code generation

Directories

Path Synopsis
bin command

Jump to

Keyboard shortcuts

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