rest

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

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

Go to latest
Published: Aug 12, 2026 License: MIT Imports: 14 Imported by: 0

README

go-rest

Build an OpenAPI 3 document from Go types, and render it as JSON or YAML.

Routes are declared with a fluent chain and payloads are described by your DTO structs — no hand-written YAML, no codegen comments, no build step. Built on kin-openapi.

This package describes an API; it does not serve one. It has no router and no handlers, so it sits alongside whatever HTTP stack you already use.

go get github.com/struckchure/go-rest

Usage

package main

import (
	"fmt"
	"log"
	"net/http"

	rest "github.com/struckchure/go-rest"
	"myapp/dto"
)

func main() {
	api := rest.New(
		rest.WithTitle("User API"),
		rest.WithVersion("1.0.0"),
		rest.WithServer("https://api.example.com", "production"),
	)

	api.AddSecurityScheme("bearerAuth", rest.BearerAuth())
	api.SetDefaultSecurity("bearerAuth")

	api.Post("/api/user/authenticate/").
		HasSummary("Authenticate a user").
		HasTags("user").
		HasRequestModel(rest.ModelOf[dto.UserAuthenticateRequestDto]()).
		HasResponseModel(http.StatusOK, rest.ModelOf[dto.UserAuthenticateResponseDto]())

	api.Get("/api/user/:id").
		RequireSecurity().
		HasRequestModel(rest.ModelOf[dto.UserGetRequestDto]()).
		HasResponseModel(http.StatusOK, rest.ModelOf[dto.UserDto]())

	out, err := api.YAML()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(out))
}

A runnable version lives in example/:

go run ./example

Models

rest.ModelOf[T]() captures a type. T can be a struct, a pointer to one, or a slice — rest.ModelOf[[]dto.UserDto]() renders as an array of $ref.

Each struct becomes a named entry under components/schemas and is referenced by $ref, so a DTO used by several routes is described once.

Binding tags

A request model binds Echo-style, and one model can draw from several places at once. json fields become the request body; param, query and header fields become parameters.

type UserGetRequestDto struct {
	Id      string  `param:"id"`
	Page    int     `query:"page"`
	Include *string `query:"include"`
	TraceId string  `header:"X-Trace-Id"`
}

A field with none of those tags is ignored entirely — it appears nowhere in the document. Tags are the only opt-in, so unexported state and internal bookkeeping fields on a DTO stay private by default.

A model made only of param/query/header fields produces no request body, so the GET above is described with four parameters and nothing else.

Required fields

A field is required unless its type can be nil. Pointers, slices, maps and interfaces can be absent; anything else cannot. Where the type alone gets it wrong, ,required and ,optional override it (,omitempty also means optional).

type LoginRequest struct {
	Email    string   `json:"email"`             // required  (not nillable)
	Remember *bool    `json:"remember"`          // optional  (nillable)
	Scopes   []string `json:"scopes"`            // optional  (nillable)
	Device   string   `json:"device,omitempty"`  // optional  (tag)
	Nonce    *string  `json:"nonce,required"`    // required  (tag beats the type)
}

The same rule drives parameters. Path parameters are always required, whatever the field says — the spec allows nothing else.

Enums

Go keeps no record of a named type's constants at runtime, so an enum has to declare itself. The way to do it is a Values method, which travels with the type and describes every field of that type, everywhere it appears:

type Status string

const (
	StatusActive   Status = "active"
	StatusArchived Status = "archived"
)

func (Status) Values() []string {
	return []string{string(StatusActive), string(StatusArchived)}
}

The method can return a slice of anything — []string, []int, []Status, []any — and can hang off the value or the pointer. Integer enums stay numeric in the document rather than turning into strings.

For a one-off on a plain type, an enum tag does the job without a named type:

type SearchRequest struct {
	Status Status   `json:"status"`                       // from the type
	Any    []Status `json:"any"`                          // values land on items
	Colour string   `json:"colour" enum:"red,green,blue"` // from a tag
	Sort   string   `query:"sort" enum:"asc,desc"`        // works on parameters too
}

Both forms follow pointers, and put the values on items for a slice rather than on the array. An enum value that does not fit its field's type — enum:"one" on an int — is an error, not a document that lies.

Security

Security is opt-in per route. A route that asks for nothing is public.

api.AddSecurityScheme("bearerAuth", rest.BearerAuth())
api.AddSecurityScheme("apiKey", rest.APIKeyAuth("X-API-Key", "header"))
api.SetDefaultSecurity("bearerAuth")

api.Post("/api/user/authenticate/")                      // public: never asked
api.Get("/api/user/:id").RequireSecurity()               // the default, bearerAuth
api.Get("/api/user/").RequireSecurity("apiKey")          // overrides the default
api.Get("/api/report/").RequireScopes("oauth2", "read:reports")

SetDefaultSecurity is the template a bare RequireSecurity() draws from; it does not secure anything by itself. No document-level security key is emitted, deliberately: OpenAPI applies that to every operation, which would leave no way to describe a public route without overriding it back to empty.

Schemes: BearerAuth(), BasicAuth(), APIKeyAuth(name, in), OIDCAuth(url), and CustomAuth(*openapi3.SecurityScheme) for anything else, such as OAuth2 flows.

Naming a scheme that was never registered, or calling RequireSecurity() with no default set, is an error rather than a silently wrong document.

Output

doc, err := api.OpenAPI()      // *openapi3.T, validated
data, err := api.JSON()        // indented JSON
data, err := api.YAML()        // YAML
err := api.WriteFile("openapi.yaml")  // .json, .yaml or .yml

The document is validated before it is returned, so a malformed spec surfaces where you build it rather than in a consumer's UI.

Route options

Method Effect
HasRequestModel(m) Request body and parameters
HasResponseModel(status, m) Response for a status code; call once per code
HasSummary(s) / HasDescription(s) Documentation
HasTags(...) Groups the operation
HasOperationId(s) Sets operationId
IsDeprecated() Marks the operation deprecated
RequireSecurity(names...) Requires auth; no arguments uses the default
RequireScopes(name, scopes...) Requires auth with OAuth2/OIDC scopes

Verbs: Get, Post, Put, Patch, Delete, Head, Options. Echo-style :id path segments are rewritten to {id} for you.

Development

go test ./...            # run the tests
go test ./... -update    # refresh the golden files in testdata/

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func APIKeyAuth

func APIKeyAuth(name, in string) *openapi3.SecurityScheme

APIKeyAuth is an API key carried in "header", "query" or "cookie".

func BasicAuth

func BasicAuth() *openapi3.SecurityScheme

BasicAuth is an HTTP basic scheme.

func BearerAuth

func BearerAuth() *openapi3.SecurityScheme

BearerAuth is an HTTP bearer scheme carrying a JWT.

func CustomAuth

func CustomAuth(scheme *openapi3.SecurityScheme) *openapi3.SecurityScheme

CustomAuth passes a scheme through untouched, for anything the helpers above do not cover (OAuth2 flows, for instance).

func OIDCAuth

func OIDCAuth(url string) *openapi3.SecurityScheme

OIDCAuth is an OpenID Connect scheme discovered at the given URL.

Types

type Model

type Model struct {
	Type reflect.Type
}

Model is a type-erased handle to a Go type used as a request or response payload. It carries no value, only the reflect.Type, so it is cheap to pass around and safe to reuse across routes.

func ModelOf

func ModelOf[T any]() Model

ModelOf captures T for schema generation:

rest.ModelOf[dto.UserAuthenticateRequestDto]()

T may be a struct, a pointer to one, or a slice of either.

func (Model) IsZero

func (m Model) IsZero() bool

IsZero reports whether the Model was never populated.

type Option

type Option func(*Rest)

Option configures a Rest instance at construction.

func WithDescription

func WithDescription(description string) Option

WithDescription sets the document description.

func WithServer

func WithServer(url, description string) Option

WithServer appends a server URL.

func WithTitle

func WithTitle(title string) Option

WithTitle sets the document title.

func WithVersion

func WithVersion(version string) Option

WithVersion sets the API version.

type Rest

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

Rest collects routes and renders them as an OpenAPI 3 document. It does not serve HTTP; it only describes it.

func New

func New(opts ...Option) *Rest

New creates a Rest instance. Title and version fall back to defaults, since OpenAPI requires both to be non-empty.

func (*Rest) AddSecurityScheme

func (r *Rest) AddSecurityScheme(name string, scheme *openapi3.SecurityScheme) *Rest

AddSecurityScheme registers a scheme under a name that routes reference with Route.HasSecurity. It ends up in components.securitySchemes.

func (*Rest) Delete

func (r *Rest) Delete(path string) *Route

Delete registers a DELETE route.

func (*Rest) Get

func (r *Rest) Get(path string) *Route

Get registers a GET route.

func (*Rest) Head

func (r *Rest) Head(path string) *Route

Head registers a HEAD route.

func (*Rest) JSON

func (r *Rest) JSON() ([]byte, error)

JSON renders the document as indented JSON.

func (*Rest) OpenAPI

func (r *Rest) OpenAPI() (*openapi3.T, error)

OpenAPI renders the registered routes as an OpenAPI 3 document. The document is validated before it is returned, so mistakes surface here rather than in a consumer's UI.

func (*Rest) Options

func (r *Rest) Options(path string) *Route

Options registers an OPTIONS route.

func (*Rest) Patch

func (r *Rest) Patch(path string) *Route

Patch registers a PATCH route.

func (*Rest) Post

func (r *Rest) Post(path string) *Route

Post registers a POST route.

func (*Rest) Put

func (r *Rest) Put(path string) *Route

Put registers a PUT route.

func (*Rest) Routes

func (r *Rest) Routes() []*Route

Routes returns the registered routes in declaration order.

func (*Rest) SetDefaultSecurity

func (r *Rest) SetDefaultSecurity(names ...string) *Rest

SetDefaultSecurity names the schemes that a bare Route.RequireSecurity() applies. Listing several means any one of them is sufficient.

It does not secure anything on its own: routes opt in. That is deliberate — a document-level `security` key would apply to every operation, and a route could then only be made public by explicitly overriding it back to empty.

func (*Rest) WriteFile

func (r *Rest) WriteFile(path string) error

WriteFile writes the document to path, choosing the encoding from the file extension: .json, .yaml or .yml.

func (*Rest) YAML

func (r *Rest) YAML() ([]byte, error)

YAML renders the document as YAML.

type Route

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

Route is a single operation: one HTTP method at one path. Every method returns the Route so calls can be chained.

func (*Route) HasDescription

func (r *Route) HasDescription(s string) *Route

HasDescription sets the operation's long description.

func (*Route) HasOperationId

func (r *Route) HasOperationId(s string) *Route

HasOperationId sets the operation's unique id.

func (*Route) HasRequestModel

func (r *Route) HasRequestModel(m Model) *Route

HasRequestModel sets the request payload. Fields tagged `json` become the request body; fields tagged `query`, `param` or `header` become parameters. A field may carry several of these tags, and untagged fields are ignored.

A field is required unless its type is nillable — a pointer, slice, map or interface can be absent, anything else cannot. Add `,optional` or `,required` to the tag where the type alone gets it wrong (`,omitempty` also means optional). Path parameters are always required.

func (*Route) HasResponseModel

func (r *Route) HasResponseModel(status int, m Model) *Route

HasResponseModel sets the payload returned for the given status code.

func (*Route) HasSummary

func (r *Route) HasSummary(s string) *Route

HasSummary sets the operation's short summary.

func (*Route) HasTags

func (r *Route) HasTags(tags ...string) *Route

HasTags appends tags used to group the operation.

func (*Route) IsDeprecated

func (r *Route) IsDeprecated() *Route

IsDeprecated marks the operation as deprecated.

func (*Route) RequireScopes

func (r *Route) RequireScopes(name string, scopes ...string) *Route

RequireScopes requires the named scheme with a set of scopes, for OAuth2 and OpenID Connect. Like RequireSecurity, repeating it records alternatives.

func (*Route) RequireSecurity

func (r *Route) RequireSecurity(names ...string) *Route

RequireSecurity requires authentication for this route.

Called with no arguments it applies whatever Rest.SetDefaultSecurity named; with arguments it overrides that default for this route alone. Listing several schemes means any one of them is sufficient.

route.RequireSecurity()          // the document default
route.RequireSecurity("apiKey")  // this route uses an API key instead

Security is opt-in: a route that never calls this is public.

Directories

Path Synopsis
dto

Jump to

Keyboard shortcuts

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