routing

package
v5.0.0 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

Documentation

Overview

Package routing provides a declarative, type-safe HTTP router that generates an OpenAPI 3 specification as routes are registered.

The primary type is Router: a concrete router that owns request decoding, validation, response encoding, error mapping, and OpenAPI accumulation. Routes are declared with typed handlers of the form func(ctx, In) (Out, error) via the package-level generic functions (Get, Post, Put, Patch, Delete, Head). Because Go interface methods cannot be generic, registration is done with these functions rather than methods on the Router.

A Router is backed by a Backend — the pluggable seam that a concrete mux library implements. Implementations ship for chi, the net/http.ServeMux standard library (stdlib), julienschmidt/httprouter, and gin-gonic/gin. The Router builds everything on top of the Backend's primitives and never depends on the library directly, so the underlying router is swappable without touching route code:

backend := chi.NewBackend(logger, tracerProvider, metricProvider, cfg)
r := routing.New(backend, encoder, logger, tracerProvider, routing.WithTitle("My API"))

routing.Post(r, "/orgs/{orgID:uint64}/users", createUser, routing.WithSummary("Create user"))
routing.Get(r, "/orgs/{orgID:uint64}", getOrg)

r.MountOpenAPI("/openapi.json", "/docs")

Path parameters use an inline typed syntax — "/users/{id:uint64}" — which drives both runtime binding and the generated parameter schema. Query, header, cookie, and body values are bound from struct tags on the typed input.

Example

Example demonstrates wiring a Router over the chi backend, registering typed routes, and mounting the generated OpenAPI spec.

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/primandproper/platform-go/v5/encoding"
	loggingnoop "github.com/primandproper/platform-go/v5/observability/logging/noop"
	metricsnoop "github.com/primandproper/platform-go/v5/observability/metrics/noop"
	tracingnoop "github.com/primandproper/platform-go/v5/observability/tracing/noop"
	"github.com/primandproper/platform-go/v5/routing"
	"github.com/primandproper/platform-go/v5/routing/backends/chi"
)

// The input for creating a user. Tags decide where each field is bound:
//   - path:  taken from the URL, cross-checked against the {orgID:uint64} token
//   - query: taken from the query string
//   - json (no location tag): part of the request body
type newUserForm struct {
	Name   string `json:"name"`
	Email  string `json:"email"`
	OrgID  uint64 `path:"orgID"`
	Notify bool   `query:"notify"`
}

// The typed output. It is encoded into the response (enveloped by default).
type person struct {
	Name  string `json:"name"`
	Email string `json:"email"`
	ID    uint64 `json:"id"`
}

// A typed handler: func(ctx, In) (Out, error). The framework decodes and
// validates In, calls this, then encodes Out — or maps a returned error to an
// HTTP status and error envelope.
func createPerson(_ context.Context, in newUserForm) (person, error) {
	return person{ID: in.OrgID*1000 + 1, Name: in.Name, Email: in.Email}, nil
}

func fetchPerson(_ context.Context, in struct {
	OrgID uint64 `path:"orgID"`
	ID    uint64 `path:"userID"`
}) (person, error) {
	return person{ID: in.ID, Name: "Ada"}, nil
}

// Example demonstrates wiring a Router over the chi backend, registering typed
// routes, and mounting the generated OpenAPI spec.
func main() {
	logger := loggingnoop.NewLogger()
	tracerProvider := tracingnoop.NewTracerProvider()
	metricsProvider := metricsnoop.NewMetricsProvider()

	// The backend is the swappable seam: chi today, gin/etc. tomorrow. It carries
	// the library-specific middleware + OpenTelemetry stack.
	backend := chi.NewBackend(logger, tracerProvider, metricsProvider, &chi.Config{
		ServiceName: "example-service",
	})

	// The Router is the declarative, OpenAPI-generating layer on top of it.
	enc := encoding.NewServerEncoderDecoder(logger, tracerProvider, encoding.ContentTypeJSON)
	r := routing.New(backend, enc, logger, tracerProvider,
		routing.WithTitle("Users API"),
		routing.WithVersion("1.0.0"),
	)

	// Typed registration is done with the package-level generic functions.
	// Path params use an inline typed syntax: {orgID:uint64}.
	routing.Post(r, "/orgs/{orgID:uint64}/users", createPerson,
		routing.WithSummary("Create a user"),
		routing.WithTags("users"),
	)

	// Group applies a shared path prefix and default tags.
	r.Group("/orgs/{orgID:uint64}", func(sub *routing.Router) {
		routing.Get(sub, "/users/{userID:uint64}", fetchPerson, routing.WithSummary("Fetch a user"))
	}, "users")

	// Serve the generated OpenAPI 3 spec (and a docs UI) on the same router.
	r.MountOpenAPI("/openapi.json", "/docs")

	// Registration errors (if any) surface here; check before serving.
	if err := r.Err(); err != nil {
		panic(err)
	}

	// In a real service you would hand r.Handler() to an http.Server (or the
	// platform's server/http package). Here we drive one request in-process.
	req := httptest.NewRequest(http.MethodPost, "/orgs/7/users?notify=true",
		strings.NewReader(`{"name":"Ada","email":"ada@example.com"}`))
	req.Header.Set(encoding.ContentTypeHeaderKey, "application/json")

	rec := httptest.NewRecorder()
	r.Handler().ServeHTTP(rec, req)

	fmt.Println("status:", rec.Code)
	fmt.Println("body:", strings.TrimSpace(rec.Body.String()))

}
Output:
status: 201
body: {"data":{"name":"Ada","email":"ada@example.com","id":7001},"details":{"currentAccountID":"","traceID":""}}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Backend

type Backend interface {
	// Handle registers handler for method at pattern. pattern uses the
	// "/users/{id}" placeholder syntax (already stripped of any type
	// annotation by the Router).
	Handle(method, pattern string, handler http.Handler)
	// Use installs global middleware, applied to every route.
	Use(middleware ...Middleware)
	// PathValue returns the value of the named path parameter for req, or ""
	// if absent.
	PathValue(req *http.Request, name string) string
	// Handler returns the composed http.Handler for serving.
	Handler() http.Handler
}

Backend is the pluggable HTTP-muxing seam beneath a Router. A concrete router library (chi, gin, ...) implements it; the Router builds every typed route, spec, and lifecycle concern on top of these primitives and never depends on the library directly.

Use must be called before Handle (many muxes, chi included, forbid adding global middleware after routes are registered).

type ContextKey

type ContextKey string

ContextKey represents strings to be used in Context objects. From the docs:

"The provided key must be comparable and should not be of type string or
 any other built-in type to avoid collisions between packages using context."

type Empty

type Empty struct{}

Empty is a placeholder type for routes that take no meaningful input or produce no response body. A route whose Out is Empty writes only a status code (no body).

type Handler

type Handler[In, Out any] func(ctx context.Context, in In) (Out, error)

Handler is a typed HTTP handler. It receives a decoded, validated input value and returns a typed output or an error. The framework handles decoding In from the request, encoding Out into the response, and mapping a returned error to an HTTP status and error envelope.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is a standard net/http middleware function.

type Option

type Option func(*routeConfig)

Option customizes a single route's registration and its generated OpenAPI operation.

func WithAdditionalResponse

func WithAdditionalResponse(status int, body any, description string) Option

WithAdditionalResponse documents an additional response (e.g. a 404 with an error body).

func WithContentType

func WithContentType(contentType encoding.ContentType) Option

WithContentType overrides the response content type for this route.

func WithDeprecated

func WithDeprecated() Option

WithDeprecated marks the operation as deprecated.

func WithDescription

func WithDescription(description string) Option

WithDescription sets the operation's long description.

func WithEnvelope

func WithEnvelope(enabled bool) Option

WithEnvelope toggles wrapping the response body in errors/http.APIResponse[Out]. Enveloping is on by default (configurable at the Router level).

func WithMiddleware

func WithMiddleware(middleware ...Middleware) Option

WithMiddleware applies middleware to this route only.

func WithOperationID

func WithOperationID(id string) Option

WithOperationID overrides the generated operation ID.

func WithResponseStatus

func WithResponseStatus(status int) Option

WithResponseStatus overrides the success HTTP status (default 200, or 201 for POST).

func WithSecurity

func WithSecurity(scheme string, scopes ...string) Option

WithSecurity adds a security requirement (scheme name + optional scopes) to the operation.

func WithSummary

func WithSummary(summary string) Option

WithSummary sets the operation's short summary.

func WithTags

func WithTags(tags ...string) Option

WithTags adds OpenAPI tags to the operation (in addition to any Router defaults).

type ParamSpec

type ParamSpec struct {
	Name  string
	Token string
}

ParamSpec is a path parameter parsed out of a typed-path pattern such as "/users/{id:uint64}". Token is the resolved type token ("string" when the pattern omitted an annotation).

type Route

type Route struct {
	Method      string
	Path        string
	OperationID string
}

Route is the descriptor returned by a registration call. It records the concrete method and (annotation-stripped) path the route was registered under, plus the resolved OpenAPI operation ID.

func Delete

func Delete[In, Out any](r *Router, pattern string, h Handler[In, Out], opts ...Option) *Route

Delete registers a typed DELETE handler.

func Get

func Get[In, Out any](r *Router, pattern string, h Handler[In, Out], opts ...Option) *Route

Get registers a typed GET handler.

func Head[In, Out any](r *Router, pattern string, h Handler[In, Out], opts ...Option) *Route

Head registers a typed HEAD handler.

func Patch

func Patch[In, Out any](r *Router, pattern string, h Handler[In, Out], opts ...Option) *Route

Patch registers a typed PATCH handler.

func Post

func Post[In, Out any](r *Router, pattern string, h Handler[In, Out], opts ...Option) *Route

Post registers a typed POST handler.

func Put

func Put[In, Out any](r *Router, pattern string, h Handler[In, Out], opts ...Option) *Route

Put registers a typed PUT handler.

type Router

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

Router is the declarative, OpenAPI-generating router. It is the primary type callers use: typed routes are registered with the package-level generic functions (Get, Post, ...), which decode and validate input, encode output, and accumulate an OpenAPI 3 operation. A Router is backed by a Backend, so the underlying mux (chi, gin, ...) is swappable without changing route code.

It is a concrete type, not an interface, because typed registration must be generic and Go does not permit generic interface methods. The swappable seam is Backend; the Router is the one fixed orchestration layer above it.

func New

func New(
	backend Backend,
	enc encoding.ServerEncoderDecoder,
	logger logging.Logger,
	tracerProvider tracing.TracerProvider,
	opts ...RouterOption,
) *Router

New builds a Router over a Backend. The backend carries all library-specific middleware and OpenTelemetry wiring; the encoder decides how request bodies are decoded and responses encoded.

func (*Router) Backend

func (r *Router) Backend() Backend

Backend returns the underlying backend, for advanced use.

func (*Router) Err

func (r *Router) Err() error

Err returns a joined error of all non-fatal registration failures accumulated so far, or nil if there were none. Check it before serving.

func (*Router) Group

func (r *Router) Group(prefix string, fn func(sub *Router), tags ...string)

Group creates a sub-Router that shares the backend, reflector, and error accumulator, but applies an additional path prefix and default tags to routes registered through it.

func (*Router) Handle

func (r *Router) Handle(method, pattern string, handler http.Handler, middleware ...Middleware)

Handle registers a raw http.Handler on the backend — an escape hatch for routes that do not fit the typed model (static files, streaming, websockets). It records no OpenAPI operation.

func (*Router) Handler

func (r *Router) Handler() http.Handler

Handler returns the composed http.Handler for serving, delegating to the backend.

func (*Router) MarshalSpec

func (r *Router) MarshalSpec() ([]byte, error)

MarshalSpec renders the accumulated spec as indented JSON.

func (*Router) MountOpenAPI

func (r *Router) MountOpenAPI(specPath, uiPath string)

MountOpenAPI registers two routes on the backend: specPath serves the spec as JSON, and (when uiPath is non-empty) uiPath serves a self-contained docs UI page that renders it. Both routes go through the backend, so they inherit all of its middleware and instrumentation.

Call this after all typed routes are registered so the served spec is complete.

func (*Router) Spec

func (r *Router) Spec() *openapi3.Spec

Spec returns the accumulated OpenAPI 3 specification. It reflects every route registered so far; call it after registration (and MountOpenAPI) is complete.

func (*Router) Use

func (r *Router) Use(middleware ...Middleware)

Use installs global middleware on the backend. Call it before registering routes.

type RouterOption

type RouterOption func(*routerConfig)

RouterOption configures a Router at construction.

func WithDefaultEnvelope

func WithDefaultEnvelope(enabled bool) RouterOption

WithDefaultEnvelope sets whether responses are wrapped in errors/http.APIResponse[Out] by default (per-route override via WithEnvelope).

func WithInfoDescription

func WithInfoDescription(description string) RouterOption

WithInfoDescription sets the OpenAPI document description.

func WithServer

func WithServer(url string) RouterOption

WithServer adds a server URL to the OpenAPI document.

func WithTitle

func WithTitle(title string) RouterOption

WithTitle sets the OpenAPI document title.

func WithVersion

func WithVersion(version string) RouterOption

WithVersion sets the OpenAPI document version.

Directories

Path Synopsis
backends
chi
gin
Package gin provides a routing.Backend built on gin-gonic/gin.
Package gin provides a routing.Backend built on gin-gonic/gin.
httprouter
Package httprouter provides a routing.Backend built on julienschmidt/httprouter, a fast radix-tree router.
Package httprouter provides a routing.Backend built on julienschmidt/httprouter, a fast radix-tree router.
internal/httpmw
Package httpmw holds the plain net/http middleware stack shared by the non-chi routing backends (stdlib, httprouter, gin).
Package httpmw holds the plain net/http middleware stack shared by the non-chi routing backends (stdlib, httprouter, gin).
stdlib
Package stdlib provides a routing.Backend built on the standard library's net/http.ServeMux.
Package stdlib provides a routing.Backend built on the standard library's net/http.ServeMux.
Package mockrouting provides mock implementations of the routing package's interfaces (currently the Backend seam), for testing routers without a real mux library.
Package mockrouting provides mock implementations of the routing package's interfaces (currently the Backend seam), for testing routers without a real mux library.

Jump to

Keyboard shortcuts

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