openapi

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 12 Imported by: 0

README

openapi

OpenAPI document generation for httpx, with automatic schema reflection and a Scalar reference UI.

It is built on top of github.com/pb33f/libopenapi for the OpenAPI document model and github.com/MarceloPetrucio/go-scalar-api-reference for the Scalar reference UI.

Installation

Install the package with go get:

go get github.com/eriicafes/httpx/openapi

Quick Start

This example wires openapi into an httpx mux, documents a route, and serves openapi JSON and Scalar docs:

package main

import (
	"net/http"

	"github.com/eriicafes/httpx"
	"github.com/eriicafes/httpx/openapi"
	"github.com/eriicafes/httpx/openapi/doc"
	"github.com/eriicafes/httpx/openapi/op"
	"github.com/eriicafes/httpx/openapi/op/resp"
)

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

type ErrorResponse struct {
	Message string `json:"message"`
}

func main() {
	mux := httpx.New()
	mux = openapi.WithRouter(mux, "My API", "1.0.0",
		doc.Server("http://localhost:8080", "Local"),
	)

	router := openapi.UseRouter(mux)

	router.Route("GET /users/{id}",
		op.Options(
			op.Summary("Get user"),
			op.PathParam[int]("id"),
			op.Response[User](200, resp.Description("User found")),
			op.Response[ErrorResponse](404, resp.Description("User not found")),
		),
		func(w http.ResponseWriter, r *http.Request) error {
			return httpx.Send(w, User{ID: 1})
		},
	)

	mux.Handle("GET /docs", router.OpenAPIJSONHandler())
	mux.Handle("GET /docs/reference", router.ReferenceHandler(nil))

	httpx.ListenAndServe(":8080", mux, nil)
}

Setup

Use WithRouter to attach OpenAPI document generation to an httpx mux:

mux := httpx.New()
mux = openapi.WithRouter(mux, "User API", "1.0.0",
	doc.Description("API description"),
	doc.Server("https://api.example.com", "Production"),
)

router := openapi.UseRouter(mux)

UseRouter walks the mux chain, so middleware, prefixes, and other wrapping muxes still apply when you register routes through the returned router.

If you want to build a document before binding to a mux, create a standalone router:

This version records paths first and leaves handler registration for later:

router := openapi.NewRouter("User API", "1.0.0",
	doc.Description("API description"),
)

router.Operation("GET /users",
	op.Summary("List users"),
	op.Response[[]User](200),
)

To register handlers later, bind that router to a mux with WithMux:

Once bound, the returned router can register handlers just like one created with WithRouter:

mux := httpx.New()
r := router.WithMux(mux)
r.Route("POST /users", op.Options(...), handler)

Registering Operations

Router supports a few ways to register operations:

Use Route when you want to document an operation and register an httpx error-returning handler at the same time:

router.Route("GET /health",
	op.Options(
		op.Summary("Health check"),
		op.OperationId("getHealth"),
		op.Response[map[string]string](200, resp.Description("Service is healthy")),
	),
	func(w http.ResponseWriter, r *http.Request) error {
		return httpx.Send(w, map[string]string{"status": "ok"})
	},
)

Router also supports Handle and HandleFunc when you already have a standard http.Handler or func(http.ResponseWriter, *http.Request).

Use Operation when you only want to record the OpenAPI operation and register the runtime handler separately:

router.Operation("GET /health", op.Options(
	op.Summary("Health check"),
	op.Response[map[string]string](200),
))

mux.HandleFunc("GET /health", healthHandler)

Schemas

Go types are reflected automatically:

These are the built-in Go-to-schema mappings used during reflection:

string                -> { "type": "string" }
bool                  -> { "type": "boolean" }
int, int64, uint, ... -> { "type": "integer" }
float32, float64      -> { "type": "number" }
[]T                   -> { "type": "array", "items": <T schema> }
map[K]V               -> { "type": "object", "additionalProperties": <V schema> }
struct                -> { "type": "object", "properties": { ... } }
*T                    -> same as T with "nullable": true
time.Time             -> { "type": "string", "format": "date-time" }

Non-pointer struct fields become required by default. Unexported fields and json:"-" fields are skipped.

Common schema helpers:

Use these helpers to add titles, examples, enums, field-level overrides, and other general schema metadata:

schema.Options(
	schema.Reference("TypeName"),
	schema.Title("Title"),
	schema.Description("Description"),
	schema.Format("uuid"),
	schema.ReadOnly(),
	schema.WriteOnly(),
	schema.Deprecated(),
	schema.Nullable(),
	schema.Default("value"),
	schema.Example("example"),
	schema.Examples("a", "b"),
	schema.Const("fixed"),
	schema.Enum("a", "b", "c"),
	schema.ExternalDocs("https://docs.example.com", "Description"),
	schema.Field("name", schema.MinLength(1)),
)

Array, object, and composition helpers use generics or schema.Type(...) getters:

These helpers cover tuple validation, object rules, and schema composition:

schema.Options(
	schema.MinItems(1),
	schema.MaxItems(10),
	schema.UniqueItems(),
	schema.PrefixItems(schema.Type[int](), schema.Type[string]()),
	schema.Contains[MyStruct](),
	schema.UnevaluatedItems[MyStruct](),
	schema.AdditionalProperties[any](),
	schema.DependentSchemas[Extra]("flag"),
	schema.PropertyNames[string](),
	schema.OneOf(schema.Type[Circle](), schema.Type[Square]()),
	schema.AnyOf(schema.Type[A](), schema.Type[B]()),
	schema.AllOf(schema.Type[A](), schema.Type[B]()),
	schema.Not[A](),
	schema.If[A](),
	schema.Then[B](),
	schema.Else[C](),
)

A type may set default schema options by implementing the schema.Schema interface:

func (User) Schema() schema.Option {
	return schema.Options(
		schema.Reference("User"),
		schema.Field("name", schema.MinLength(3), schema.MaxLength(50)),
		schema.Field("age", schema.Minimum(18), schema.Maximum(120)),
		schema.Field("email", schema.Email(), schema.Example("user@example.com")),
	)
}
Union Types

Types from github.com/eriicafes/union expand automatically.

An untagged union expands its cases into anyOf:

type Shape union.Union[ShapeSpec]

type ShapeSpec struct {
	Circle *Circle
	Square *Square
}

That produces an untagged anyOf across Circle and Square.

Tagged unions expand into oneOf and add a discriminator-backed wrapper object for each case:

type Shape union.TaggedUnion[ShapeSpec]

That produces a tagged oneOf with a discriminator, using "type" and "value" by default.

Document Options

Document metadata is configured with doc.Option values passed to WithRouter or NewRouter:

Use doc options for top-level metadata, external docs, security requirements, and component registration:

mux = openapi.WithRouter(mux, "User API", "2.0.0",
	doc.Version("3.1.0"),
	doc.Summary("Short summary"),
	doc.Description("Full description of the API"),
	doc.TermsOfService("https://example.com/terms"),
	doc.Contact("API Team", "https://example.com", "api@example.com"),
	doc.License("MIT", "https://opensource.org/licenses/MIT", ""),

	doc.Server("http://localhost:8080", "Local"),
	doc.Server("https://{region}.api.example.com", "Production",
		server.Variable("region", "us-east",
			server.VariableEnum("us-east", "eu-west"),
			server.VariableDescription("Deployment region"),
		),
	),

	doc.Tag("users", "User management"),
	doc.Tag("health", "Service health",
		tag.ExternalDocs("https://docs.example.com/health", "Health check docs"),
	),

	doc.ExternalDocs("https://docs.example.com", "Full documentation"),
	doc.SecurityScheme("bearerAuth",
		securityscheme.HTTP("bearer"),
		securityscheme.BearerFormat("JWT"),
	),
	doc.Security("bearerAuth"),
)
Server Options

Use server.Option values to refine doc.Server(...) or pathitem.Server(...) entries:

doc.Server("https://{region}.api.example.com", "Production",
	server.Name("production"),
	server.Variable("region", "us-east",
		server.VariableEnum("us-east", "eu-west"),
		server.VariableDescription("Deployment region"),
	),
)
Tag Options

Use tag.Option values to enrich tags added through doc.Tag(...):

doc.Tag("health", "Service health",
	tag.Summary("Operational endpoints"),
	tag.ExternalDocs("https://docs.example.com/health", "Health check docs"),
)
Security Scheme Options

Use securityscheme.Option values when registering reusable schemes with doc.SecurityScheme(...):

doc.SecurityScheme("bearerAuth",
	securityscheme.Description("JWT bearer authentication"),
	securityscheme.HTTP("bearer"),
	securityscheme.BearerFormat("JWT"),
)

Path Item Options

Use pathitem.Option values for path-level metadata, shared parameters, and server overrides:

path := pathitem.New(
	pathitem.Summary("Health endpoints"),
	pathitem.Description("Operations for checking service health"),
	pathitem.Parameter[string](param.InHeader, "X-Region"),
	pathitem.Server("https://internal.example.com", "Internal"),
)

Use pathitem.Path when you want to group multiple methods and shared path-level settings under one route:

path := pathitem.New(
	pathitem.Description("Health endpoints"),
)

path.Route(http.MethodGet,
	op.Options(
		op.Summary("Get health"),
		op.Response[map[string]string](200),
	),
	getHealth,
)

path.Route(http.MethodPost,
	op.Options(
		op.Summary("Run health check"),
		op.Response[map[string]string](200),
	),
	runHealthCheck,
)

router.Path("/health", path)

Operation Options

Combine operation-level settings with op.Options:

op.Options(
	op.Summary("Short summary"),
	op.Description("Full description"),
	op.OperationId("uniqueOperationId"),
	op.Tags("users", "admin"),
	op.Deprecated(),
	op.ExternalDocs("https://docs.example.com", "External docs"),
	op.Server("https://api.example.com", "Override server"),
	op.Security("bearerAuth", "scope1"),
	op.PathParam[int]("id"),
	op.QueryParam[string]("search"),
	op.HeaderParam[string]("X-API-Key"),
	op.CookieParam[string]("session"),
	op.RequestBody[CreateUserRequest](),
	op.Response[User](200),
	op.Response[op.NoContent](204),
)

Parameter Options

This example shows how to refine a generated parameter with descriptions, examples, serialization rules, and a component reference:

op.QueryParam[string]("search",
	param.Description("Filter by name or email"),
	param.Required(true),
	param.Deprecated(),
	param.Example("john"),
	param.Style("form"),
	param.Explode(true),
	param.AllowEmptyValue(),
	param.AllowReserved(),
	param.NamedExample("basic", "john"),
	param.Reference("Search"),
)

A type may set default parameter options by implementing the param.Parameter interface:

func (T) Parameter() param.Option

those defaults are applied before inline options.

Request Bodies

op.RequestBody[T] creates a request body for T and adds application/json by default. Use op.NoContent when an operation has no body.

Add request-body metadata, alternate media types, or store the body as a reusable component:

op.RequestBody[CreateUserRequest](
	body.Description("User details"),
	body.Required(true),
	body.ContentType[CreateUserRequest]("application/xml"),
	body.Reference("CreateUser"),
)

A type may set default request body options by implementing the body.RequestBody interface:

func (T) RequestBody() body.Option

those defaults are applied before inline options.

Media Type Options

Use mediatype.Option values to refine generated content entries for request and response bodies:

body.ContentType[CreateUserRequest]("application/json",
	mediatype.Example(CreateUserRequest{Name: "Ada"}),
	mediatype.NamedExample("sample", CreateUserRequest{Name: "Grace"}),
	mediatype.Encoding("avatar", mediatype.EncodingContentType("image/png")),
)

A type may set default media type options by implementing the mediatype.MediaType interface:

func (T) MediaType() mediatype.Option

those defaults are applied before inline options.

Responses

op.Response[T](status, ...) uses http.StatusText(status) as the default description and adds application/json content unless T is op.NoContent.

Responses can declare headers, links, alternate media types, and reusable component references:

op.Response[User](200,
	resp.Description("User found"),
	resp.Summary("Successful response"),
	resp.Header[string]("X-Request-Id",
		header.Description("Unique request trace ID"),
		header.Required(true),
	),
	resp.Link("getUser",
		link.OperationId("getUser"),
		link.Parameter("userId", "$response.body#/id"),
		link.Description("Fetch the user by ID"),
	),
	resp.ContentType[User]("application/xml"),
	resp.Reference("UserResponse"),
)

A type may set default response options by implementing the resp.Response interface:

func (T) Response() resp.Option

those defaults are applied before inline options.

Header Options

Use header.Option values for response headers:

resp.Header[string]("X-Request-Id",
	header.Description("Unique request trace ID"),
	header.Style("simple"),
	header.Example("req_123"),
	header.NamedExample("sample", "req_456"),
)

A type may set default header options by implementing the header.Header interface:

func (T) Header() header.Option

those defaults are applied before inline options.

Use link.Option values to describe follow-up operations available from a response:

resp.Link("getUser",
	link.OperationId("getUser"),
	link.Parameter("userId", "$response.body#/id"),
	link.Description("Fetch the created user by ID"),
	link.Server("https://api.example.com", "Production"),
)

Example Options

Use example.Option values when adding named examples to parameters, headers, or media types:

param.NamedExample("basic", "healthy",
	example.Summary("Basic health state"),
	example.Description("Returned when the service is healthy"),
)

Callback Options

Use callback.Option values to attach callbacks to operations:

op.Options(
	op.Callback("onHealthChange",
		callback.Reference("HealthStatusChanged"),
	),
)

Advanced Patterns

A single type may implement more than one interface at a time. This lets one type provide defaults at multiple OpenAPI layers.

This is especially useful with Reference(...) options. A type can set its own component reference through an interface-provided default, which keeps call sites short and reuses the same component everywhere.

For example, a response type can set default schema, response and media type options at the same time:

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

func (User) Schema() schema.Option {
	return schema.Options(
		// Register the reflected schema in components/schemas.
		schema.Reference("User"),
		schema.Field("name", schema.MinLength(1)),
	)
}

func (User) Response() resp.Option {
	return resp.Options(
		// Register the response in components/responses.
		resp.Reference("UserResponse"),
		resp.Header[string]("X-Resource-Type",
			header.Example("user"),
		),
	)
}

func (User) MediaType() mediatype.Option {
	return mediatype.Options(
		mediatype.NamedExample("default", User{
			ID:   1,
			Name: "Ada",
		}),
	)
}

op.Response[User](200)

Likewise, a request type can combine schema, request body, and media type defaults:

type CreateUserRequest struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

func (CreateUserRequest) Schema() schema.Option {
	return schema.Options(
		schema.Reference("CreateUserRequest"),
		schema.Field("email", schema.Email()),
	)
}

func (CreateUserRequest) RequestBody() body.Option {
	return body.Options(
		body.Description("Payload for creating a user"),
		body.Reference("CreateUserBody"),
	)
}

func (CreateUserRequest) MediaType() mediatype.Option {
	return mediatype.Options(
		mediatype.NamedExample("default", CreateUserRequest{
			Name:  "Ada",
			Email: "ada@example.com",
		}),
	)
}

op.RequestBody[CreateUserRequest]()

References also work inline. One call site can define the full object, and another can reuse it by reference:

// First call site: define the reusable response and store it in components/responses.
op.Response[User](200,
	resp.Description("User response"),
	resp.Header[string]("X-Request-Id",
		header.Description("Unique request trace ID"),
	),
	resp.Reference("UserResponse"),
)

// Later call site: reuse the stored component by reference.
op.Response[User](200,
	resp.Reference("UserResponse"),
)

Inline options are skipped only when the type itself sets Reference(...) through an implemented interface. That keeps the stored component unambiguous even when different call sites pass different inline options.

Serving the Document

Serve the generated document directly as JSON or YAML:

mux.Handle("GET /docs/openapi.json", router.OpenAPIJSONHandler())
mux.Handle("GET /docs/openapi.yaml", router.OpenAPIYAMLHandler())

To serve Scalar:

Use the built-in Scalar handler when the default embedded configuration is enough:

mux.Handle("GET /docs/reference", router.ReferenceHandler(nil))

Or with custom options:

Pass scalar.Options to point Scalar at a hosted spec URL or customize the page:

mux.Handle("GET /docs", router.ReferenceHandler(&scalar.Options{
	SpecURL: "http://localhost:8080/docs/openapi.json",
	Theme:   scalar.ThemeDefault,
	CustomOptions: scalar.CustomOptions{
		PageTitle: "My API Reference",
	},
}))

If you need the raw document:

You can always access the underlying *v3.Document from github.com/pb33f/libopenapi/datamodel/high/v3 for custom rendering or validation:

doc := router.GetDocument()
yaml, err := doc.Render()
json, err := doc.RenderJSON("")

Documentation

Overview

Package openapi builds an OpenAPI document alongside httpx route registration.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func WithRouter

func WithRouter(mux httpx.ServeMux, title, version string, opts ...doc.Option) httpx.Mux

WithRouter wraps mux with an embedded OpenAPI Router configured by opts. Use UseRouter to retrieve the Router and register OpenAPI operations and route handlers.

Types

type Router

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

Router builds an OpenAPI document alongside route registration. Use NewRouter to create a standalone router, or UseRouter to extract one from a mux chain created with WithRouter.

func NewRouter

func NewRouter(title, version string, opts ...doc.Option) *Router

NewRouter creates a standalone Router with the given document options. The router has no mux bound to it. Call WithMux before registering routes.

func UseRouter

func UseRouter(mux httpx.ServeMux) *Router

UseRouter extracts a Router from the mux chain created by WithRouter. The passed in mux is used to register routes, so any mux layered on top of WithRouter (Middleware, Prefix, Fallback, etc.) are still applied. Panics if no Router is found in the chain.

func (*Router) GetDocument

func (r *Router) GetDocument() *v3.Document

GetDocument returns the underlying OpenAPI document.

func (*Router) Handle

func (r *Router) Handle(pattern string, opt op.Option, handler http.Handler)

Handle registers an OpenAPI operation and a handler on the underlying mux.

func (*Router) HandleFunc

func (r *Router) HandleFunc(pattern string, opt op.Option, handler func(http.ResponseWriter, *http.Request))

HandleFunc registers an OpenAPI operation and a handler func on the underlying mux.

func (*Router) OpenAPIJSONHandler

func (r *Router) OpenAPIJSONHandler() http.HandlerFunc

OpenAPIJSONHandler returns an http.HandlerFunc that renders the OpenAPI document as JSON.

func (*Router) OpenAPIYAMLHandler

func (r *Router) OpenAPIYAMLHandler() http.HandlerFunc

OpenAPIYAMLHandler returns an http.HandlerFunc that renders the OpenAPI document as YAML.

func (*Router) Operation

func (r *Router) Operation(pattern string, opt op.Option)

Operation registers an OpenAPI operation for the given pattern. The pattern follows the same "METHOD /path" format as the Go ServeMux. If no method is present the operation is registered for all supported methods. Use this when the handler is registered separately.

func (*Router) Path

func (r *Router) Path(pattern string, p *pathitem.Path)

Path registers an OpenAPI pathitem and its registered handlers for each method.

func (*Router) ReferenceHandler

func (r *Router) ReferenceHandler(options *scalar.Options) http.HandlerFunc

ReferenceHandler returns an http.HandlerFunc that serves a Scalar API reference UI. When options is nil the document is embedded directly in the HTML and the page title is set to the API title. Pass a non-nil *scalar.Options to override the default options.

func (*Router) Route

func (r *Router) Route(pattern string, opt op.Option, handler func(http.ResponseWriter, *http.Request) error)

Route registers an OpenAPI operation and a route handler on the underlying mux.

func (*Router) WithMux

func (r *Router) WithMux(mux httpx.ServeMux) *Router

WithMux returns a copy of the Router bound to mux. The document and schema registry are shared with the original Router.

Jump to

Keyboard shortcuts

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