humanats

package module
v0.0.0-...-c3306d8 Latest Latest
Warning

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

Go to latest
Published: Dec 2, 2025 License: MIT Imports: 17 Imported by: 0

README

huma-nats

Go Reference Go Report Card

A Huma adapter for NATS microservices. This package enables you to define APIs using Huma's powerful features (struct-based validation, OpenAPI generation, etc.) while using NATS request/response for transport.

Features

  • Huma Integration: Full support for Huma's struct-based input/output validation
  • OpenAPI Generation: Automatic OpenAPI schema generation for your NATS services
  • Subject Mapping: HTTP-style paths mapped to NATS subjects (GET /users/{id}users.*.GET)
  • Query Parameters: Support for query parameters via configurable headers
  • Prefix Support: Optional subject prefix for namespacing
  • NATS Micro: Built on NATS micro service framework for service discovery

Installation

go get github.com/sandrolain/huma-nats

Quick Start

package main

import (
    "context"
    "log"

    "github.com/danielgtaylor/huma/v2"
    "github.com/nats-io/nats.go"
    humanats "github.com/sandrolain/huma-nats"
)

// Input/Output structs
type GetUserInput struct {
    ID string `path:"id"`
}

type GetUserOutput struct {
    Body User
}

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func main() {
    // Connect to NATS
    nc, err := nats.Connect(nats.DefaultURL)
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Close()

    // Create Huma API with NATS adapter
    api := humanats.New(nc, huma.DefaultConfig("User API", "1.0.0"),
        humanats.WithServiceName("user-service"),
        humanats.WithServiceVersion("1.0.0"),
    )

    // Register endpoints
    huma.Get(api, "/users/{id}", func(ctx context.Context, input *GetUserInput) (*GetUserOutput, error) {
        return &GetUserOutput{
            Body: User{
                ID:   input.ID,
                Name: "John Doe",
            },
        }, nil
    })

    // Start the service
    adapter := api.Adapter().(*humanats.NATSAdapter)
    if err := adapter.Start(); err != nil {
        log.Fatal(err)
    }

    log.Println("Service started. Press Ctrl+C to stop.")
    select {} // Block forever
}

Subject Mapping

HTTP paths are mapped to NATS subjects with the method as the last segment, following NATS subject naming conventions:

HTTP Method Path NATS Subject
GET /users users.GET
GET /users/{id} users.*.GET
POST /users users.POST
PUT /users/{id} users.*.PUT
DELETE /users/{id} users.*.DELETE
GET /orgs/{orgId}/teams/{teamId} orgs.*.teams.*.GET

With prefix:

HTTP Method Path Prefix NATS Subject
GET /users/{id} api.v1 api.v1.users.*.GET

Configuration Options

api := humanats.New(nc, humaConfig,
    // Subject prefix
    humanats.WithPrefix("api.v1"),
    
    // Service metadata
    humanats.WithServiceName("my-service"),
    humanats.WithServiceVersion("1.0.0"),
    
    // Queue group for load balancing
    humanats.WithQueueGroup("workers"),
    
    // Custom header names
    humanats.WithQueryHeader("X-Custom-Query"),
    humanats.WithStatusHeader("X-Custom-Status"),
    
    // Custom subject mapper
    humanats.WithSubjectMapper(myCustomMapper),
)

Query Parameters

Query parameters are passed via the X-Query header (configurable) using URL-encoded format:

// Client side
msg := nats.NewMsg("GET.users")
msg.Header.Set("X-Query", "limit=10&offset=20&search=john")
resp, err := nc.RequestMsg(msg, timeout)

Response Headers

The response includes an X-Status header (configurable) with the HTTP status code:

// Response headers
resp.Header.Get("X-Status") // "200", "404", "500", etc.

Client Package

The client package provides a convenient HTTP-like interface for making requests to huma-nats services:

package main

import (
    "log"
    "time"

    "github.com/nats-io/nats.go"
    "github.com/sandrolain/huma-nats/client"
)

func main() {
    nc, err := nats.Connect(nats.DefaultURL)
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Close()

    // Create client
    c := client.New(nc,
        client.WithClientPrefix("api.v1"),
        client.WithClientTimeout(10*time.Second),
    )

    // GET request
    resp, err := c.Get("/users/123")
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Status: %d, Body: %s", resp.Status(), resp.String())

    // GET with query parameters
    resp, err = c.Get("/users",
        client.WithQuery("limit", "10"),
        client.WithQuery("offset", "0"),
    )

    // POST with JSON body
    type User struct {
        Name string `json:"name"`
    }
    resp, err = c.Post("/users", User{Name: "John"},
        client.WithHeader("Authorization", "Bearer token"),
    )

    // Check response
    if resp.IsSuccess() {
        var user User
        resp.JSON(&user)
        log.Printf("Created: %+v", user)
    }
}
Client Options
c := client.New(nc,
    client.WithClientPrefix("api.v1"),           // Subject prefix
    client.WithClientTimeout(10*time.Second),    // Default timeout
    client.WithClientHeader("X-API-Key", "key"), // Default headers
    client.WithClientQueryHeader("X-Query"),     // Custom query header
    client.WithClientStatusHeader("X-Status"),   // Custom status header
)
Request Options
// Headers
client.WithHeader("X-Custom", "value")
client.WithHeaders(map[string]string{"X-A": "1", "X-B": "2"})
client.WithAuthorization("Bearer token")
client.WithBearerToken("my-token")
client.WithBasicAuth("base64-encoded")
client.WithContentType("application/json")
client.WithAccept("application/xml")

// Query parameters
client.WithQuery("key", "value")
client.WithQueryMap(map[string]string{"a": "1", "b": "2"})
client.WithQueryValues(url.Values{"tags": []string{"go", "nats"}})

// Body
client.WithBody([]byte("raw bytes"))
client.WithBodyString("text body")
client.WithJSON(myStruct)

// Timeout
client.WithTimeout(5*time.Second)
Response Methods
resp.Status()        // HTTP status code (int)
resp.IsSuccess()     // true if 2xx
resp.IsError()       // true if 4xx or 5xx
resp.IsClientError() // true if 4xx
resp.IsServerError() // true if 5xx

resp.Body()          // []byte
resp.String()        // string
resp.JSON(&v)        // unmarshal to struct

resp.Header("name")  // get header value
resp.Headers()       // all headers (nats.Header)
resp.Subject()       // NATS subject
resp.Raw()           // underlying *nats.Msg

HTTP Gateway

The cmd/gateway tool provides an HTTP server that proxies requests to NATS-based huma-nats services and serves OpenAPI documentation.

Features
  • HTTP to NATS request translation
  • Multiple documentation UIs: Swagger UI, Scalar (default), Stoplight Elements
  • OpenAPI spec forwarding from NATS services
  • CORS support for browser access
  • Graceful shutdown
Installation
go install github.com/sandrolain/huma-nats/cmd/gateway@latest
Usage
# Start gateway with default settings
./gateway

# Connect to a specific NATS server with prefix
./gateway -nats nats://my-server:4222 -prefix api.v1

# Use Swagger UI instead of Scalar
./gateway -docs-ui swagger -title "My API"
Environment Variables
export NATS_URL=nats://localhost:4222
export NATS_PREFIX=api.v1
export DOCS_UI=scalar
export API_TITLE="My API"
export HTTP_PORT=8080
./gateway
Endpoints
Path Description
/docs API documentation UI
/openapi.json OpenAPI spec (JSON)
/openapi.yaml OpenAPI spec (YAML)
/* Proxied to NATS service
Architecture
┌─────────────┐     HTTP      ┌─────────────┐     NATS      ┌─────────────┐
│   Browser   │ ───────────▶  │   Gateway   │ ───────────▶  │  huma-nats  │
│             │ ◀───────────  │             │ ◀───────────  │   Service   │
└─────────────┘               └─────────────┘               └─────────────┘
                                    │
                                    │ /docs, /openapi.json
                                    ▼
                              ┌─────────────┐
                              │  Swagger/   │
                              │  Scalar UI  │
                              └─────────────┘

Raw Client Example

package main

import (
    "encoding/json"
    "log"
    "time"

    "github.com/nats-io/nats.go"
)

func main() {
    nc, err := nats.Connect(nats.DefaultURL)
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Close()

    // Simple GET request
    resp, err := nc.Request("GET.users.123", nil, 5*time.Second)
    if err != nil {
        log.Fatal(err)
    }

    var user struct {
        ID   string `json:"id"`
        Name string `json:"name"`
    }
    json.Unmarshal(resp.Data, &user)
    log.Printf("User: %+v", user)

    // GET with query parameters
    msg := nats.NewMsg("GET.users")
    msg.Header.Set("X-Query", "limit=10&search=john")
    resp, err = nc.RequestMsg(msg, 5*time.Second)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Response: %s", resp.Data)

    // POST request
    msg = nats.NewMsg("POST.users")
    msg.Data = []byte(`{"name": "New User"}`)
    msg.Header.Set("Content-Type", "application/json")
    resp, err = nc.RequestMsg(msg, 5*time.Second)
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Created: %s", resp.Data)
}

Service Discovery

The adapter registers as a NATS micro service, enabling service discovery:

// Get service info
adapter := api.Adapter().(*humanats.NATSAdapter)
info := adapter.Info()
log.Printf("Service: %s v%s", info.Name, info.Version)

// Get registered endpoints
endpoints := adapter.Endpoints()
for _, ep := range endpoints {
    log.Printf("  %s %s -> %s", ep.Method, ep.Path, ep.Subject)
}

Custom Subject Mapper

You can implement a custom subject mapper:

type MySubjectMapper struct{}

func (m *MySubjectMapper) ToSubject(method, path string) string {
    // Custom logic
    return "my.custom.subject"
}

func (m *MySubjectMapper) ExtractParams(pathTemplate, subject string) map[string]string {
    // Custom parameter extraction
    return map[string]string{}
}

// Use it
api := humanats.New(nc, config, humanats.WithSubjectMapper(&MySubjectMapper{}))

Error Handling

Huma errors are properly translated to NATS responses:

huma.Get(api, "/users/{id}", func(ctx context.Context, input *GetUserInput) (*GetUserOutput, error) {
    if input.ID == "notfound" {
        return nil, huma.Error404NotFound("user not found")
    }
    // ...
})

The response will include:

  • X-Status: 404
  • JSON error body from Huma

Running Tests

# Unit tests
task test

# Integration tests (requires Docker)
task test:integration

# All tests with coverage
task test:coverage

Requirements

  • Go 1.21+
  • NATS Server 2.x
  • Huma v2

License

MIT License - see LICENSE for details.

Documentation

Overview

Package humanats provides a Huma adapter for NATS microservices. It enables defining APIs using Huma's powerful schema validation and OpenAPI generation capabilities while using NATS request/response as the transport layer.

Package humanats provides a Huma adapter for NATS microservices.

This package enables building APIs using Huma's powerful schema validation and OpenAPI generation capabilities while using NATS request/response as the transport layer instead of HTTP.

Overview

The adapter translates HTTP-style operations defined with Huma into NATS subjects and handlers. This allows you to:

  • Define APIs using Huma's familiar struct tags and validation
  • Generate OpenAPI documentation for your NATS services
  • Use NATS request/response for communication between microservices
  • Leverage NATS micro's built-in service discovery and monitoring

Subject Mapping

HTTP paths are mapped to NATS subjects with the method at the end, following NATS subject naming conventions:

GET /users/{id}        -> users.*.GET
POST /users            -> users.POST
PUT /users/{id}        -> users.*.PUT
DELETE /users/{id}     -> users.*.DELETE

With an optional prefix (e.g., "api"):

GET /users/{id}        -> api.users.*.GET

Headers

HTTP semantics are preserved using NATS headers:

  • Query parameters: Sent in the "X-Query" header as URL-encoded string
  • Response status: Returned in the "X-Status" header

Example Usage

package main

import (
    "context"
    "log"

    "github.com/danielgtaylor/huma/v2"
    "github.com/nats-io/nats.go"
    "github.com/sandrolain/huma-nats/src/humanats"
)

type GreetingInput struct {
    Name string `path:"name" doc:"Name to greet"`
}

type GreetingOutput struct {
    Body struct {
        Message string `json:"message"`
    }
}

func main() {
    nc, err := nats.Connect(nats.DefaultURL)
    if err != nil {
        log.Fatal(err)
    }
    defer nc.Close()

    api := humanats.New(nc, huma.DefaultConfig("Greeting Service", "1.0.0"),
        humanats.WithServiceName("GreetingService"),
    )

    huma.Get(api, "/hello/{name}", func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
        return &GreetingOutput{
            Body: struct{ Message string }{
                Message: "Hello, " + input.Name + "!",
            },
        }, nil
    })

    adapter := api.Adapter().(*humanats.NATSAdapter)
    if err := adapter.Start(); err != nil {
        log.Fatal(err)
    }
    defer adapter.Stop()

    // Keep running
    select {}
}

Index

Constants

View Source
const DefaultQueryHeader = "X-Query"

DefaultQueryHeader is the default header name for query parameters.

View Source
const DefaultStatusHeader = "X-Status"

DefaultStatusHeader is the default header name for HTTP status codes.

Variables

View Source
var (
	// ErrServiceNotStarted is returned when trying to use the adapter before starting.
	ErrServiceNotStarted = errors.New("humanats: service not started")

	// ErrServiceAlreadyStarted is returned when trying to start an already running service.
	ErrServiceAlreadyStarted = errors.New("humanats: service already started")

	// ErrNilConnection is returned when a nil NATS connection is provided.
	ErrNilConnection = errors.New("humanats: nil NATS connection")

	// ErrEmptyServiceName is returned when an empty service name is provided.
	ErrEmptyServiceName = errors.New("humanats: empty service name")

	// ErrEmptyServiceVersion is returned when an empty service version is provided.
	ErrEmptyServiceVersion = errors.New("humanats: empty service version")

	// ErrMultipartNotSupported is returned when multipart forms are requested over NATS.
	ErrMultipartNotSupported = errors.New("humanats: multipart forms not supported over NATS")

	// ErrEndpointRegistration is returned when an endpoint fails to register.
	ErrEndpointRegistration = errors.New("humanats: failed to register endpoint")
)

Package errors

Functions

func New

func New(nc *nats.Conn, humaConfig huma.Config, opts ...Option) huma.API

New creates a new Huma API with the NATS adapter. The service must be started with Start() after registering operations.

func PathToSubjectPattern

func PathToSubjectPattern(method, path, prefix string) string

PathToSubjectPattern converts a Huma path to a NATS subject subscription pattern. This is useful for understanding what subject pattern will be used.

Types

type AdapterConfig

type AdapterConfig struct {
	// ServiceName is the name of the NATS micro service.
	ServiceName string

	// ServiceVersion is the version of the NATS micro service.
	ServiceVersion string

	// Prefix is an optional prefix for all NATS subjects.
	// Example: "api" results in subjects like "api.GET.users"
	Prefix string

	// Headers configures the NATS header names.
	Headers HeaderConfig

	// QueueGroup is the optional queue group for load balancing.
	// If empty, the default NATS micro queue group "q" is used.
	QueueGroup string
}

AdapterConfig contains the configuration for the NATS adapter.

type DefaultSubjectMapper

type DefaultSubjectMapper struct {
	// Prefix is an optional prefix for all subjects.
	Prefix string
}

DefaultSubjectMapper implements SubjectMapper with a conservative mapping strategy. The format is: {Prefix}.{path}.{METHOD} where path segments are separated by dots. This follows NATS subject naming conventions where the action (method) comes last.

func (*DefaultSubjectMapper) ExtractParams

func (m *DefaultSubjectMapper) ExtractParams(pathTemplate, subject string) map[string]string

ExtractParams extracts path parameter values from a NATS subject. It matches the subject against the path template and extracts values where wildcards (*) appear in the subject. Subject format: {prefix}.{path}.{method}

func (*DefaultSubjectMapper) ToSubject

func (m *DefaultSubjectMapper) ToSubject(method, path string) string

ToSubject converts an HTTP method and path to a NATS subject. Examples:

  • GET, "/users" -> "users.GET" (without prefix)
  • GET, "/users" -> "api.users.GET" (with prefix "api")
  • GET, "/users/{id}" -> "users.*.GET"
  • GET, "/users/{id}/posts/{postId}" -> "users.*.posts.*.GET"

type EndpointInfo

type EndpointInfo struct {
	Method      string
	Path        string
	OperationID string
	Subject     string
}

EndpointInfo contains public information about a registered endpoint.

type HeaderConfig

type HeaderConfig struct {
	// QueryHeader is the header name for query parameters (URL-encoded).
	// Default: "X-Query"
	QueryHeader string

	// StatusHeader is the header name for HTTP response status codes.
	// Default: "X-Status"
	StatusHeader string
}

HeaderConfig configures the NATS header names used for HTTP semantics.

func DefaultHeaderConfig

func DefaultHeaderConfig() HeaderConfig

DefaultHeaderConfig returns a HeaderConfig with default values.

type NATSAdapter

type NATSAdapter struct {
	// contains filtered or unexported fields
}

NATSAdapter implements huma.Adapter for NATS microservices.

func NewAdapter

func NewAdapter(nc *nats.Conn, opts ...Option) *NATSAdapter

NewAdapter creates a NATSAdapter without creating the Huma API. This is useful for advanced use cases where you need more control.

func (*NATSAdapter) Config

func (a *NATSAdapter) Config() AdapterConfig

Config returns the adapter configuration.

func (*NATSAdapter) Endpoints

func (a *NATSAdapter) Endpoints() []EndpointInfo

Endpoints returns information about registered endpoints. Useful for debugging and documentation.

func (*NATSAdapter) GetSubject

func (a *NATSAdapter) GetSubject(method, path string) string

GetSubject returns the NATS subject for a given HTTP method and path. This is useful for clients that need to know what subject to publish to.

func (*NATSAdapter) Handle

func (a *NATSAdapter) Handle(op *huma.Operation, handler func(huma.Context))

Handle registers a handler for a Huma operation. This is called by Huma when registering operations.

func (*NATSAdapter) Info

func (a *NATSAdapter) Info() micro.Info

Info returns information about the running service. Returns empty values if the service hasn't been started.

func (*NATSAdapter) ServeHTTP

func (a *NATSAdapter) ServeHTTP(w http.ResponseWriter, _ *http.Request)

ServeHTTP implements http.Handler for compatibility. This is a no-op for NATS as we don't serve HTTP directly. Use StartHTTPBridge() if HTTP access is needed.

func (*NATSAdapter) Service

func (a *NATSAdapter) Service() micro.Service

Service returns the underlying NATS micro service. Returns nil if the service hasn't been started.

func (*NATSAdapter) Start

func (a *NATSAdapter) Start() error

Start starts the NATS micro service and registers all endpoints. This must be called after registering all Huma operations.

func (*NATSAdapter) Started

func (a *NATSAdapter) Started() bool

Started returns whether the service is running.

func (*NATSAdapter) Stats

func (a *NATSAdapter) Stats() micro.Stats

Stats returns statistics about the running service. Returns empty values if the service hasn't been started.

func (*NATSAdapter) Stop

func (a *NATSAdapter) Stop() error

Stop stops the NATS micro service.

type Option

type Option func(*NATSAdapter)

Option is a function that configures the NATSAdapter.

func WithPrefix

func WithPrefix(prefix string) Option

WithPrefix sets a prefix for all NATS subjects. Example: WithPrefix("api") results in subjects like "api.users.GET"

func WithQueryHeader

func WithQueryHeader(header string) Option

WithQueryHeader sets a custom header name for query parameters.

func WithQueueGroup

func WithQueueGroup(queueGroup string) Option

WithQueueGroup sets the queue group for load balancing.

func WithServiceName

func WithServiceName(name string) Option

WithServiceName sets the NATS micro service name.

func WithServiceVersion

func WithServiceVersion(version string) Option

WithServiceVersion sets the NATS micro service version.

func WithStatusHeader

func WithStatusHeader(header string) Option

WithStatusHeader sets a custom header name for HTTP status codes.

func WithSubjectMapper

func WithSubjectMapper(mapper SubjectMapper) Option

WithSubjectMapper sets a custom subject mapper.

type SubjectMapper

type SubjectMapper interface {
	// ToSubject converts an HTTP method and path to a NATS subject.
	// Example: GET, "/users/{id}" -> "users.*.GET"
	ToSubject(method, path string) string

	// ExtractParams extracts path parameter values from a NATS subject
	// using the original path template.
	// Example: "/users/{id}", "users.123.GET" -> {"id": "123"}
	ExtractParams(pathTemplate, subject string) map[string]string
}

SubjectMapper defines the interface for mapping HTTP paths to NATS subjects.

Directories

Path Synopsis
Package client provides a convenient HTTP-like client for NATS-based microservices built with huma-nats.
Package client provides a convenient HTTP-like client for NATS-based microservices built with huma-nats.
examples
basic command
Package main demonstrates basic usage of huma-nats adapter.
Package main demonstrates basic usage of huma-nats adapter.
basic/client command
Package main demonstrates a NATS client calling the greeting service.
Package main demonstrates a NATS client calling the greeting service.
crud command
Package main demonstrates CRUD operations with huma-nats adapter.
Package main demonstrates CRUD operations with huma-nats adapter.
crud/client command
Package main demonstrates a NATS client calling the CRUD user service.
Package main demonstrates a NATS client calling the CRUD user service.
query-params command
Package main demonstrates query parameters with huma-nats adapter.
Package main demonstrates query parameters with huma-nats adapter.
query-params/client command
Package main demonstrates a NATS client using query parameters.
Package main demonstrates a NATS client using query parameters.

Jump to

Keyboard shortcuts

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