Help us shape the future of Go! Take the 2026 Go Developer Survey

goenax

package module
v0.1.0 Latest Latest
Warning

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

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

README

Goenax

Code-as-source-of-truth API registry for Go.

Declare each HTTP endpoint once — in the same place you mount it on your router — and Goenax builds an OpenAPI 3.1 document from your Go types. From that one source you generate the Postman collection and the TypeScript types with the standard tools, and Goenax gives you the checks that keep them honest.

Framework-neutral: adapters for Echo, net/http and Gin ship in the box; the core imports none of them.

go get github.com/Lev2098/Goenax

Why

Your API contract already exists in the code. Writing it down a second time — a hand-kept OpenAPI file, a Postman collection, doc comments — creates a copy that drifts the first week a handler changes and the copy doesn't. Annotation tools move the drift into comments the compiler never checks. Goenax takes the contract from the Go types you already have, so the schema can't describe a shape the compiler doesn't know.


Quickstart

import (
    "net/http"

    "github.com/labstack/echo/v4"
    "github.com/Lev2098/Goenax"
    echoadapter "github.com/Lev2098/Goenax/adapter/echo"
)

type LoginRequest struct {
    Email    string `json:"email"    validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}
type SessionResponse struct {
    Token string `json:"token"`
}

e := echo.New()
reg := goenax.New()
api := echoadapter.New(e.Group("/api"), reg, "/api")

api.POST("/login", loginHandler, []goenax.Option{
    goenax.Summary("Exchange credentials for a session"),
    goenax.Owner("#team"),
    goenax.Tags("Auth"),
    goenax.Request[LoginRequest](),
    goenax.Response[SessionResponse](http.StatusOK),
    goenax.Error(http.StatusUnauthorized, "invalid", "wrong email or password"),
}, authMiddleware)

// one call did two things: mounted the live route on Echo,
// and recorded its contract in reg.

spec, _ := reg.OpenAPI(goenax.Info{
    Title: "My API", Version: "1.0.0",
    Servers: []goenax.Server{{URL: "http://localhost:8080"}},
})

To emit the spec in CI without booting the server, keep the declarations in a table that both the live mount and a handler-free Spec() consume (the option sets don't need handlers) — then a tiny cmd prints Spec().OpenAPI(...).


The declaration API

Option Documents
Summary(s) · Description(s) human text
Owner(s) owning team/channel — required by Validate
Tags(t...) grouping (OpenAPI tags, Postman folders)
Request[T]() request body type
Response[T](status) success body type + status
NoContent(status) success with no body
HTML() success renders text/html
Also(status, desc) · AlsoJSON[T](status, desc) an extra status, without / with a body
RespHeader(name, desc) a header on the success response
Error(status, code, msg) a documented failure
Query / Header / PathParam parameters
Secured() requires a bearer session
DependsOn(ids...) endpoints a QA run must call first
Compose(opts...) bundle options (e.g. a middleware's own docs)

Request[T] / Response[T] are generic: reflect.TypeFor[T]() binds the schema to the exact struct at compile time — no annotations.


Schema derivation

Goenax reflects over the request/response types and reads the json and go-playground validate tags:

Go OpenAPI schema
primitives string / integer / boolean / number
*T, or json:",omitempty" optional
validate:"required" required
validate:"email" / "uuid" / "url" format
validate:"min/max/len" minLength/maxLength or minimum/maximum
validate:"oneof=a b c" enum
example:"…" example (else a deterministic default)
time.Time / []T / map[string]T date-time / array / object with additionalProperties
named struct a $ref into components/schemas (defined once, shared)

It is cycle-safe and gives every leaf a deterministic example, so a generated Postman collection is stable across runs.


Adapters

Same shape for every framework — New, Group, Handle/GET/POST/DELETE. Each mounts the route and records the contract. Only the adapter imports the framework.

echoadapter.New(e.Group("/api"), reg, "/api")   // *echo.Echo / *echo.Group
nethttpadapter.New(mux, reg, "/api")             // *http.ServeMux (Go 1.22+ patterns)
ginadapter.New(engine, reg, "/api")              // gin.IRouter

Middleware uses each framework's native type. echoadapter.Routes(e) / ginadapter.Routes(engine) list the registered routes for Coverage (below).


Keeping it honest

Goenax gives you the checks; wire them into your tests and CI.

  • reg.Validate() — returns problems per endpoint: missing summary/owner/tag, a mutating method with no error response, a duplicate method+path, or a path {token} that doesn't match a declared PathParam. Fail CI on a non-empty result and documentation becomes a build requirement, not a reviewer's ask.
  • reg.ValidateResponse(method, path, body) — validates a real response against its declared Response[T] schema. Call it from a handler test and a handler that returns the wrong shape fails — closing the gap between what an endpoint promises and what it returns.
  • reg.Coverage(actual) — diffs the routes registered on the router against the declared endpoints, flagging routes that are undocumented or documented but never mounted.
  • Regenerate & diff. Commit the generated spec (and Postman / TS) and, in CI, regenerate them and diff against the committed files — a change that isn't reflected fails the build.
  • Breaking changes. Diff the spec against your baseline with oasdiff and fail on a breaking change.

Postman and TypeScript come from the emitted OpenAPI via openapi-to-postman and openapi-typescript — Goenax doesn't reinvent them.


License

MIT — see LICENSE.

Documentation

Overview

Package goenax is a code-as-source-of-truth registry for HTTP endpoints.

An endpoint is declared once — typed request/response plus metadata — and the registry projects that declaration into OpenAPI (and, downstream, Postman and TypeScript). Because the request/response schemas are taken from real Go types, the contract cannot lie about the shapes the handler binds and returns.

The package is deliberately self-contained (standard library only) so it can later be lifted into its own module/repo with no untangling. See DESIGN.md.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Endpoint

type Endpoint struct {
	Method string
	Path   string

	Summary string
	Desc    string
	Owner   string
	Tags    []string

	Request  reflect.Type // nil = no request body
	Response reflect.Type // nil = empty response (e.g. 204)
	Status   int          // success status code (200/201/204)

	Secured     bool // true = requires a bearer session
	SuccessHTML bool // success renders text/html rather than JSON
	Params      []Param
	RespHeaders []ResponseHeader // headers on the success response
	Also        []ExtraResponse
	Errors      []ErrorSpec
	DependsOn   []string // IDs of endpoints that must run first (QA scenarios)
}

Endpoint is the full contract of a single route: how it is called plus the metadata our policy gate requires. Request and Response hold the Go types of the bodies (nil when there is none); their JSON schemas are derived from those types in a later phase.

func Define

func Define(method, path string, opts ...Option) Endpoint

Define assembles an Endpoint from a method, path, and options. Status defaults to 200 unless an option overrides it. Define does no validation — enforcing required metadata is the policy gate's job in a later phase; here we only build the declaration.

type ErrorSpec

type ErrorSpec struct {
	Status  int
	Code    string // short machine-readable slug, e.g. "validation"
	Message string // human-readable example message
}

ErrorSpec is one documented non-2xx outcome of an endpoint.

type ExtraResponse

type ExtraResponse struct {
	Status int
	Desc   string
	Type   reflect.Type
}

ExtraResponse is an additional documented response (e.g. a 202 alternative to the primary success). Type is optional — nil means the response has no body.

type Info

type Info struct {
	Title   string
	Version string
	Servers []Server
}

Info is the OpenAPI `info` block plus the servers the API is reachable at.

type Option

type Option func(*Endpoint)

Option mutates an Endpoint during Define.

This is the functional-options pattern: each helper below returns a small function that sets one field, and Define applies them in order. It keeps declarations readable and lets us add new metadata later without changing every call site.

func Also

func Also(status int, desc string) Option

Also documents an additional response status alongside the primary success, without a body.

func AlsoJSON

func AlsoJSON[T any](status int, desc string) Option

AlsoJSON documents an additional response status that carries a JSON body of type T (e.g. a 202 with a { "status": … } payload).

func Compose

func Compose(opts ...Option) Option

Compose bundles several options into one. It lets a middleware ship its own documentation — e.g. a rate limiter that contributes a 429 response, or a captcha that contributes a header and its rejection — so routes opt into the behaviour and its docs together.

func DependsOn

func DependsOn(ids ...string) Option

DependsOn records endpoint IDs that must be called before this one.

func Description

func Description(s string) Option

Description sets the longer description.

func Error

func Error(status int, code, message string) Option

Error documents one non-2xx outcome.

func HTML

func HTML() Option

HTML marks the success response as text/html (e.g. email-link landing pages).

func Header(name string, required bool, desc string) Option

Header documents a request header (e.g. X-Recaptcha-Token).

func NoContent

func NoContent(status int) Option

NoContent declares a success with an empty body, e.g. goenax.NoContent(204).

func Owner

func Owner(s string) Option

Owner sets the owning team/channel (e.g. "#team").

func PathParam

func PathParam(name, desc string) Option

PathParam documents a path parameter (always required, must appear as {name} in the route path).

func Query

func Query(name string, required bool, desc string) Option

Query documents a query parameter.

func Request

func Request[T any]() Option

Request declares the request body type, written as goenax.Request[fooRequest]().

It is generic so the caller names the type instead of passing a value. reflect.TypeFor[T]() (Go 1.22+) captures that type at compile time, so the schema is bound to the exact struct the handler binds — no annotations, and the compiler guarantees the type exists.

func RespHeader

func RespHeader(name, desc string) Option

RespHeader documents a header returned on the success response.

func Response

func Response[T any](status int) Option

Response declares the success body type and status, e.g. goenax.Response[sessionResponse](200).

func Secured

func Secured() Option

Secured marks the endpoint as requiring a bearer session. Unmarked endpoints are public (they emit an empty security requirement).

func Summary

func Summary(s string) Option

Summary sets the one-line summary.

func Tags

func Tags(tags ...string) Option

Tags appends grouping tags (folders in Postman, tags in OpenAPI).

type Param

type Param struct {
	Name     string
	In       string // "query"
	Required bool
	Desc     string
}

Param is a query (or, later, path) parameter.

type Registry

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

func New

func New() *Registry

func (*Registry) Add

func (r *Registry) Add(e Endpoint)

func (*Registry) Coverage

func (r *Registry) Coverage(actual []Route) (undocumented, unmounted []Route)

Coverage compares the routes actually registered on a router with the endpoints declared in the registry. A shared route table makes the two match by construction; Coverage catches the case a table can't — a route mounted (or removed) directly, out of band. It returns:

  • undocumented: on the router but not in the registry (missing from the spec)
  • unmounted: in the registry but never registered (a dead spec entry)

Pass the router's routes via an adapter helper, e.g. echoadapter.Routes(e) or ginadapter.Routes(engine). (net/http's ServeMux exposes no route list, so it has no helper — feed it a Route slice you build yourself.)

func (*Registry) Endpoints

func (r *Registry) Endpoints() []Endpoint

func (*Registry) Lookup

func (r *Registry) Lookup(method, path string) (Endpoint, bool)

func (*Registry) OpenAPI

func (r *Registry) OpenAPI(info Info) ([]byte, error)

OpenAPI projects the registry into an OpenAPI 3.1 document and returns it as indented JSON. Map keys (paths, methods, status codes) serialise in a stable order, so the output is deterministic and safe to diff / drift-check.

func (*Registry) Validate

func (r *Registry) Validate() []ValidationError

Validate checks every endpoint against the policy and structural rules, and returns one ValidationError per offending endpoint (nil = all good).

Policy (kept small, not bureaucratic): every endpoint needs a summary, description, owner and tag; mutating methods need at least one error response. Structural: no two endpoints share a method+path, and path {tokens} match the declared PathParam names. Request bodies and the success status are not checked (action POSTs carry no body; the status is auto-defaulted).

func (*Registry) ValidateResponse

func (r *Registry) ValidateResponse(method, path string, body []byte) error

ValidateResponse checks that a JSON response body conforms to the schema of an endpoint's declared Response type. It closes the gap between what an endpoint *promises* (Response[T]) and what the handler actually returns: call it from a handler test on the real response, and a shape that has drifted from the contract fails the test.

rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if err := reg.ValidateResponse("POST", "/api/login", rec.Body.Bytes()); err != nil {
    t.Fatalf("response breaks its contract: %v", err)
}

It checks the declared shape (types, required properties, enums) and allows unknown extra properties, so a forward-compatible addition doesn't fail a test.

type ResponseHeader

type ResponseHeader struct {
	Name string
	Desc string
}

ResponseHeader documents a header returned on the success response.

type Route

type Route struct {
	Method string
	Path   string
}

Route is a method + path pair — the unit of comparison for Coverage.

type Schema

type Schema struct {
	Ref                  string             `json:"$ref,omitempty"`
	Type                 string             `json:"type,omitempty"`
	Format               string             `json:"format,omitempty"`
	Enum                 []string           `json:"enum,omitempty"`
	Example              any                `json:"example,omitempty"`
	MinLength            *int               `json:"minLength,omitempty"`
	MaxLength            *int               `json:"maxLength,omitempty"`
	Minimum              *float64           `json:"minimum,omitempty"`
	Maximum              *float64           `json:"maximum,omitempty"`
	Properties           map[string]*Schema `json:"properties,omitempty"`
	Items                *Schema            `json:"items,omitempty"`
	AdditionalProperties *Schema            `json:"additionalProperties,omitempty"`
	Required             []string           `json:"required,omitempty"`
}

Schema is a JSON Schema node — the shared shape behind an endpoint's request and response bodies. It maps directly onto the schema object OpenAPI 3.1 uses. When Ref is set the node is only a reference into components/schemas.

func SchemaFor

func SchemaFor(t reflect.Type) Schema

SchemaFor builds an inline Schema from a Go type (no $ref). It is cycle-safe: a self-referential type stops at a bare object rather than recursing forever.

type Server

type Server struct {
	URL         string
	Description string
}

Server is one OpenAPI server entry.

type ValidationError

type ValidationError struct {
	Endpoint string   // "POST /api/contact"
	Problems []string // ["description", "owner", "duplicate route"]
}

ValidationError lists everything wrong with one endpoint — missing metadata, a duplicate route, or a path/parameter mismatch.

func (ValidationError) Error

func (e ValidationError) Error() string

Directories

Path Synopsis
adapter
echo
Package echoadapter mounts routes on an Echo router while recording each route's contract in an goenax.Registry — one declaration, two outputs: a live route and a spec entry.
Package echoadapter mounts routes on an Echo router while recording each route's contract in an goenax.Registry — one declaration, two outputs: a live route and a spec entry.
gin
Package ginadapter mounts routes on a Gin router while recording each route's contract in an goenax.Registry — one declaration, two outputs.
Package ginadapter mounts routes on a Gin router while recording each route's contract in an goenax.Registry — one declaration, two outputs.
nethttp
Package nethttpadapter mounts routes on a net/http ServeMux while recording each route's contract in an goenax.Registry — one declaration, two outputs.
Package nethttpadapter mounts routes on a net/http ServeMux while recording each route's contract in an goenax.Registry — one declaration, two outputs.

Jump to

Keyboard shortcuts

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