typed

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 23 Imported by: 0

README

typed

Typed handlers and automatic OpenAPI 3.1 docs for the chi router. The handler's input and output types are the documentation: no annotations, no code generation.

Install

go get github.com/Brihith/typed

Quick start

Imports omitted; see examples/users for the full program.

type GetUserInput struct {
	ID      int  `path:"id" minimum:"1"`
	Verbose bool `query:"verbose" doc:"Include extra fields"`
}

type CreateUserInput struct {
	Body struct {
		Name  string `json:"name" required:"true" minLength:"1" maxLength:"100"`
		Email string `json:"email" required:"true" format:"email"`
		Role  string `json:"role,omitempty" enum:"admin,member" default:"member"`
	}
}

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email" example:"ada@example.com"`
}

func main() {
	r := chi.NewRouter()
	api := typed.New(r, typed.Config{Title: "Users API", Version: "1.0.0"})

	typed.Get(api, "/users/{id}", getUser, typed.Summary("Get a user"), typed.Tags("users"))
	typed.Post(api, "/users", createUser, typed.Status(http.StatusCreated), typed.Tags("users"))

	http.ListenAndServe(":8080", r) // also serves /openapi.json and /docs
}

func getUser(ctx context.Context, in *GetUserInput) (*User, error) {
	if in.ID != 1 {
		return nil, typed.NotFound("user not found")
	}
	return &User{ID: 1, Name: "Ada", Email: "ada@example.com"}, nil
}

func createUser(ctx context.Context, in *CreateUserInput) (*User, error) {
	// in.Body is decoded and validated; Role defaults to "member".
	return &User{ID: 2, Name: in.Body.Name, Email: in.Body.Email}, nil
}

A complete runnable version is in examples/users. Run it and open http://localhost:8080/docs.

Handlers have the form func(ctx context.Context, in *In) (*Out, error). Use struct{} as Out for an empty 204 response.

Input tags

Each exported field of In is read from one source.

Tag / name Source Notes
path:"name" URL path parameter always required
query:"name" query string slices read repeated keys (?tag=a&tag=b)
header:"Name" request header
field Body JSON request body any JSON-encodable type

Parameter types: string, bool, integers, floats, slices of those (query only), and anything implementing encoding.TextUnmarshaler. A pointer parameter is nil when absent.

Untagged embedded structs (not pointers) are flattened, so shared parameters such as pagination can live in their own struct. Any other exported field without one of these tags is a registration error.

Validation tags

Tags apply to body fields and parameters. Each one validates the request and appears in the generated schema.

Tag Applies to Meaning
required:"true" any non-zero value (non-nil pointer, non-empty string or slice)
minLength / maxLength string rune count
pattern string Go regexp, compiled at registration
format string email, uuid, date-time, date, uri are validated; others are documentation only
enum:"a,b,c" string, numbers allowed values
minimum / maximum numbers inclusive bounds
minItems / maxItems slices length bounds
default params, body fields applied when absent or zero (slices: comma-separated list); shown in docs
doc any description in docs
example any example in docs

Because Go cannot tell "missing" from "zero", required means non-zero. For non-pointer fields the zero value (0, "", false, an empty slice) counts as "not sent" and skips all rules. Use a pointer (*int, *string) to enforce rules on zero values or to require presence. A non-pointer nested struct is always validated; use *Nested for an optional object. For custom checks, implement Validate() error on the input type; return typed.FieldError(location, message) to control the response.

Errors

Errors are RFC 9457 Problem Details with Content-Type: application/problem+json. All validation failures in a request are returned together:

{
  "type": "about:blank",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "validation failed",
  "errors": [{ "location": "body.email", "message": "must be a valid email", "value": "nope" }]
}

Malformed JSON and wrong content types give 400, oversized bodies 413, unparsable parameters and validation failures 422. A handler returning a *typed.Error (typed.NotFound, typed.Conflict, ...) controls the status; any other error becomes a 500 with a generic message, and the original is logged.

To change the error format, set Config.ErrorHandler:

typed.New(r, typed.Config{
	Title:   "Users API",
	Version: "1.0.0",
	ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
		// write your own response
	},
})

Sub-routers and middleware

api.Route("/admin", func(admin *typed.API) {
	admin = admin.With(requireAdmin) // any chi middleware
	typed.Delete(admin, "/users/{id}", deleteUser)
})

Routes registered under Route appear in the spec with the prefix. Plain chi routes keep working next to typed ones; they are just not documented. api.OpenAPI() returns the spec, for example to write it to a file in CI.

Status

Beta. The API may change before v1.

License

MIT. See LICENSE.

Documentation

Overview

Package typed registers typed handlers on a chi router and generates an OpenAPI 3.1 document from their input and output types. See the README and examples/users for usage. For non-pointer fields the zero value counts as "not sent" and skips validation rules; use a pointer to enforce rules on zero values or to require presence.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Delete

func Delete[In, Out any](api *API, path string, h Handler[In, Out], opts ...RouteOption)

Delete registers a DELETE route.

func Get

func Get[In, Out any](api *API, path string, h Handler[In, Out], opts ...RouteOption)

Get registers a GET route.

func Patch

func Patch[In, Out any](api *API, path string, h Handler[In, Out], opts ...RouteOption)

Patch registers a PATCH route.

func Post

func Post[In, Out any](api *API, path string, h Handler[In, Out], opts ...RouteOption)

Post registers a POST route.

func Put

func Put[In, Out any](api *API, path string, h Handler[In, Out], opts ...RouteOption)

Put registers a PUT route.

Types

type API

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

API registers typed routes on a chi router and builds the OpenAPI document.

func New

func New(r chi.Router, cfg Config) *API

New returns an API that registers its routes on r. It panics when the configuration is invalid.

func (*API) OpenAPI

func (a *API) OpenAPI() *OpenAPI

OpenAPI returns the document describing the routes registered so far.

func (*API) Route

func (a *API) Route(pattern string, fn func(sub *API))

Route mounts a sub-router at pattern and passes fn an API for it. Routes registered on sub appear in the document under pattern.

func (*API) Router

func (a *API) Router() chi.Router

Router returns the underlying chi router.

func (*API) With

func (a *API) With(mw ...func(http.Handler) http.Handler) *API

With returns an API whose routes run behind the given middleware.

type Components

type Components struct {
	Schemas map[string]*Schema `json:"schemas,omitempty"`
}

Components holds reusable objects of the document.

type Config

type Config struct {
	// Title and Version are required and appear in the OpenAPI info object.
	Title       string
	Version     string
	Description string
	// SpecPath serves the OpenAPI document (default "/openapi.json").
	// DocsPath serves the documentation UI (default "/docs"). "-" disables
	// either.
	SpecPath string
	DocsPath string
	// MaxBodyBytes limits request bodies (default 1 MiB).
	MaxBodyBytes int64
	// ErrorHandler replaces the default problem+json error writer.
	ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
}

Config configures an API.

type Error

type Error struct {
	Type   string        `json:"type"`
	Title  string        `json:"title"`
	Status int           `json:"status"`
	Detail string        `json:"detail,omitempty"`
	Errors []ErrorDetail `json:"errors,omitempty"`
}

Error is an RFC 9457 problem details response.

func BadRequest

func BadRequest(detail string) *Error

BadRequest returns a 400 error.

func Conflict

func Conflict(detail string) *Error

Conflict returns a 409 error.

func FieldError

func FieldError(location, message string) *Error

FieldError returns a 422 validation error for a single field.

func Forbidden

func Forbidden(detail string) *Error

Forbidden returns a 403 error.

func InternalServerError

func InternalServerError(detail string) *Error

InternalServerError returns a 500 error.

func NewError

func NewError(status int, detail string) *Error

NewError returns a problem with the given HTTP status and detail.

func NotFound

func NotFound(detail string) *Error

NotFound returns a 404 error.

func Unauthorized

func Unauthorized(detail string) *Error

Unauthorized returns a 401 error.

func UnprocessableEntity

func UnprocessableEntity(detail string) *Error

UnprocessableEntity returns a 422 error.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

type ErrorDetail

type ErrorDetail struct {
	Location string `json:"location"`
	Message  string `json:"message"`
	Value    any    `json:"value,omitempty"`
}

ErrorDetail describes a single problem, such as one invalid field.

type Handler

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

Handler is a typed request handler.

type Info

type Info struct {
	Title       string `json:"title,omitempty"`
	Version     string `json:"version,omitempty"`
	Description string `json:"description,omitempty"`
}

Info describes the API.

type MediaType

type MediaType struct {
	Schema *Schema `json:"schema,omitempty"`
}

MediaType holds the schema for one content type.

type OpenAPI

type OpenAPI struct {
	OpenAPI    string               `json:"openapi"`
	Info       Info                 `json:"info"`
	Paths      map[string]*PathItem `json:"paths"`
	Components *Components          `json:"components,omitempty"`
}

OpenAPI is the root of an OpenAPI 3.1 document.

type Operation

type Operation struct {
	OperationID string               `json:"operationId,omitempty"`
	Summary     string               `json:"summary,omitempty"`
	Description string               `json:"description,omitempty"`
	Tags        []string             `json:"tags,omitempty"`
	Deprecated  bool                 `json:"deprecated,omitempty"`
	Parameters  []*Parameter         `json:"parameters,omitempty"`
	RequestBody *RequestBody         `json:"requestBody,omitempty"`
	Responses   map[string]*Response `json:"responses"`
}

Operation describes one HTTP operation.

type Parameter

type Parameter struct {
	Name        string  `json:"name,omitempty"`
	In          string  `json:"in,omitempty"`
	Description string  `json:"description,omitempty"`
	Required    bool    `json:"required,omitempty"`
	Schema      *Schema `json:"schema,omitempty"`
}

Parameter describes a path, query, header or cookie parameter.

type PathItem

type PathItem struct {
	Get    *Operation `json:"get,omitempty"`
	Put    *Operation `json:"put,omitempty"`
	Post   *Operation `json:"post,omitempty"`
	Delete *Operation `json:"delete,omitempty"`
	Patch  *Operation `json:"patch,omitempty"`
}

PathItem groups the operations available on one path.

type RequestBody

type RequestBody struct {
	Required bool                  `json:"required,omitempty"`
	Content  map[string]*MediaType `json:"content,omitempty"`
}

RequestBody describes the body an operation accepts.

type Response

type Response struct {
	Description string                `json:"description"`
	Content     map[string]*MediaType `json:"content,omitempty"`
}

Response describes one response of an operation.

type RouteOption

type RouteOption func(*routeConfig)

RouteOption customizes a route and its operation.

func Deprecated

func Deprecated() RouteOption

Deprecated marks the operation as deprecated.

func Description

func Description(s string) RouteOption

Description sets the operation description.

func OperationID

func OperationID(id string) RouteOption

OperationID overrides the generated operation ID.

func Status

func Status(code int) RouteOption

Status sets the success status code.

func Summary

func Summary(s string) RouteOption

Summary sets the operation summary.

func Tags

func Tags(tags ...string) RouteOption

Tags sets the operation tags.

type Schema

type Schema struct {
	Ref                  string             `json:"$ref,omitempty"`
	Type                 any                `json:"type,omitempty"`
	Format               string             `json:"format,omitempty"`
	Description          string             `json:"description,omitempty"`
	Pattern              string             `json:"pattern,omitempty"`
	ContentEncoding      string             `json:"contentEncoding,omitempty"`
	Items                *Schema            `json:"items,omitempty"`
	AdditionalProperties *Schema            `json:"additionalProperties,omitempty"`
	Properties           map[string]*Schema `json:"properties,omitempty"`
	Required             []string           `json:"required,omitempty"`
	AnyOf                []*Schema          `json:"anyOf,omitempty"`
	Enum                 []any              `json:"enum,omitempty"`
	Examples             []any              `json:"examples,omitempty"`
	Default              any                `json:"default,omitempty"`
	MinLength            *int               `json:"minLength,omitempty"`
	MaxLength            *int               `json:"maxLength,omitempty"`
	MinItems             *int               `json:"minItems,omitempty"`
	MaxItems             *int               `json:"maxItems,omitempty"`
	Minimum              *float64           `json:"minimum,omitempty"`
	Maximum              *float64           `json:"maximum,omitempty"`
}

Schema is a JSON Schema (2020-12) object as used by OpenAPI 3.1. Type is a string, or a []string when the value may also be null.

Directories

Path Synopsis
examples
users command
Command users is a small Users API built with typed.
Command users is a small Users API built with typed.

Jump to

Keyboard shortcuts

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