An open API service indexing awesome lists of open source software.

https://github.com/mrz1836/go-actions

🎬 Typed HTTP action framework with OpenAPI 3.1 generation for chi
https://github.com/mrz1836/go-actions

actions chi go golang http router

Last synced: about 12 hours ago
JSON representation

🎬 Typed HTTP action framework with OpenAPI 3.1 generation for chi

Awesome Lists containing this project

README

          

# 🎬  go-actions

**Typed HTTP actions for Go — one declaration, a handler and an OpenAPI 3.1 contract that can't drift**


Release
Go Version
License




CI / CD   


Build
Last Commit


     Quality   


Go Report
Coverage



Security   


Scorecard
Security


     Docs   


Go Reference
Bitcoin




### Project Navigation



📦 Installation


⚡ Quick Start


🧪 Examples & Tests




📚 Documentation


🛠️ Code Standards


📊 Benchmarks




🤖 AI Usage


⚖️ License


👥 Maintainers


## 🧩 About

**go-actions** is a typed HTTP action framework with **OpenAPI 3.1 generation** for
[chi](https://github.com/go-chi/chi). You declare a route **once** as a typed
`Action[Req, Resp]`, and a `Registry` turns each declaration — all from the **same
reflected types** — into:

- an `http.HandlerFunc` (decode → validate → handle → encode),
- a JSON Schema 2020-12,
- an OpenAPI 3.1 document (JSON **and** YAML), and
- a browsable HTML/Markdown action index.

Because every artifact derives from the same types, the **published contract cannot
drift from runtime behavior**. `Freeze()` enforces invariants at startup — unique IDs,
unique method+path, and documented statuses — so a misconfigured route fails on boot,
not in production.

- **One declaration, many artifacts** — handler, schema, OpenAPI, and docs all generated from one typed struct.
- **Production-safe by default** — panic recovery, a 1 MiB request-body cap, request-id propagation, and JSON `404`/`405` responses are on out of the box, every one overridable.
- **Composable middleware** — registry-wide (`WithMiddleware`) and per-action (`Action.Middleware`) using the standard chi/`net-http` signature, for auth, CORS, logging, and rate limiting.
- **Observability hook** — one `WithObserver` callback receives each request's action id, status, latency, and error — the seam for access logs, metrics, and tracing.
- **Documented auth** — declare OpenAPI `securitySchemes` and per-operation `security` (with `BearerAuth`/`APIKeyAuth` helpers) so the contract states how to authenticate.
- **Pluggable error mapping** — decouple your domain errors from the wire shape with an `ErrorMapper`.
- **Self-documenting** — serve `/openapi.json`, `/openapi.yaml`, and a browsable `/_actions` index straight from the registry.
- **Struct-tag validation, with an escape hatch** — `required`, `min`, `max`, `oneof`, `uuid`, `email`, `e164`, `rfc3339` (the same tags feed the JSON Schema), plus a `Validatable` interface for rules a tag can't express.
- **No domain coupling** — the core imports only the standard library, `go-chi/chi/v5`, `google/uuid`, and `gopkg.in/yaml.v3`.

> Why it matters: in 2026, your API contract is consumed by SDK generators, API
> gateways, and AI agents calling tools. A contract generated from the code that
> actually serves traffic is one you never have to hand-reconcile.


## 📦 Installation

**go-actions** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy).
```shell script
go get -u github.com/mrz1836/go-actions
```

Get the [MAGE-X](https://github.com/mrz1836/mage-x) build tool for development:
```shell script
go install github.com/mrz1836/mage-x/cmd/magex@latest
```


## ⚡ Quick Start

### 1. Declare an action

An `Action[Req, Resp]` declares one route. Request fields bind from the JSON body or
from `path`/`query`/`header` tags; `validate` tags are enforced before your handler runs.

```go
package main

import (
"context"
"net/http"

"github.com/mrz1836/go-actions"
)

type createUserReq struct {
Name string `json:"name" validate:"required,min=1,max=64"`
Email string `json:"email" validate:"required,email"`
}

type user struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

func createUser() actions.Action[createUserReq, actions.Created[user]] {
return actions.Action[createUserReq, actions.Created[user]]{
ID: "users.create",
Method: http.MethodPost,
Path: "/users",
Summary: "Create a user",
Tags: []string{"users"},
Statuses: []actions.StatusDoc{
{Code: http.StatusCreated, Description: "the created user"},
{Code: http.StatusUnprocessableEntity, Description: "invalid body", Error: true},
},
Handle: func(_ context.Context, req createUserReq) (actions.Created[user], error) {
u := user{ID: "u_1", Name: req.Name, Email: req.Email}
return actions.Created[user]{Body: u}, nil
},
}
}
```

Response envelopes set the documented status: `actions.Empty` → 204,
`actions.Created[T]` → 201, `actions.Accepted[T]` → 202. Returning any other value
encodes as 200. For full control, return `actions.Response[T]` to set a custom status
and response headers (e.g. `Cache-Control`/`ETag`), `actions.Page[T]` for a
conventional cursor-paginated list body, or `actions.List[T]` for a non-paginated
collection — `{"items": [...], "meta": {"total": N}}`. Build the latter with
`actions.NewList(items)`, which sets the total and normalizes a nil slice to `[]`:

```go
Handle: func(ctx context.Context, _ listReq) (actions.List[user], error) {
return actions.NewList(store.all()), nil // 200 {"items":[...],"meta":{"total":N}}
}
```

Path, query, and header parameters bind by struct tag (`path:`, `query:`,
`header:`). Scalars convert automatically — `string`, `bool`, the integer and
float kinds, and `time.Time` (parsed as RFC3339 or a bare `2006-01-02` date; a
malformed value yields a 422). Any of these may be a pointer, so an absent
optional parameter stays `nil` rather than a zero value:

```go
type listReq struct {
Active *bool `json:"-" query:"active"` // nil when ?active= is absent
Since *time.Time `json:"-" query:"since"` // RFC3339; 422 if malformed
Limit int `json:"-" query:"limit" validate:"max=100"`
}
```

### 2. Register, freeze, and mount

```go
func main() {
reg := actions.NewRegistry(actions.WithInfo(
"Users API",
"Manage users.",
"1.0.0",
))

actions.Register(reg, createUser())
reg.Freeze() // validates declarations and builds the contract artifacts

http.Handle("/", reg.Handler())
_ = http.ListenAndServe(":8080", nil)
}
```

`Register` is the only typed seam — after `Freeze()` the registry is sealed and further
`Register` calls panic. `Handler()` returns an `http.Handler` mounting every action plus
the self-documentation endpoints.

### 3. Serve the contract

`Handler()` mounts three self-documenting endpoints automatically:

| Endpoint | Returns |
| ---------------- | --------------------------------------------------- |
| `/openapi.json` | the OpenAPI 3.1 document as JSON |
| `/openapi.yaml` | the same document as YAML |
| `/_actions` | a browsable HTML index (Markdown via `Accept`) |

The `/_actions` index ships in core and is always on — no build tag required. You can
also reach the raw bytes directly with `reg.OpenAPIJSON()` and `reg.OpenAPIYAML()` (for
example, to write a committed snapshot).


Pluggable error handling

Handlers return ordinary Go errors. An `ErrorMapper` translates them into the
transport-level `APIError`, decoupling the framework from your domain error model:

```go
type APIError struct {
Status int
Code string
Message string
Fields []FieldError
}

type ErrorMapper func(error) APIError
```

Install one with `WithErrorMapper`:

```go
reg := actions.NewRegistry(actions.WithErrorMapper(func(err error) actions.APIError {
if errors.Is(err, ErrNotFound) {
return actions.APIError{
Status: http.StatusNotFound,
Code: actions.CodeNotFound,
Message: "resource not found",
}
}
return actions.APIError{
Status: http.StatusInternalServerError,
Code: actions.CodeInternal,
Message: "an internal error occurred",
}
}))
```

The default mapper passes an `*APIError` through unchanged (so handlers may return one
directly) and maps every other error to a redacted 500, ensuring internal detail never
reaches the wire. The error envelope is always
`{"error": ..., "code": ..., "request_id": ...}`.

**foundationx adapter** — the optional `foundationx` sub-package provides a ready-made
`ErrorMapper` that wires the [`go-foundation`](https://github.com/mrz1836/go-foundation)
error model (`*ValidationError` → 422, `ErrNotFound` → 404). It lives in its own package
so consumers that do not use `go-foundation` never pull it into their module graph:

```go
import "github.com/mrz1836/go-actions/foundationx"

reg := actions.NewRegistry(actions.WithErrorMapper(foundationx.NewErrorMapper()))
```

Registry options

| Option | Effect |
| ------------------------------- | ------------------------------------------------------------------- |
| `WithInfo(title, desc, version)`| Sets the OpenAPI `info` block; the title also names the `_actions` index. |
| `WithErrorMapper(mapper)` | Installs a custom error mapper (replaces the default generic one). |
| `WithStripPrefix(prefix)` | Strips a namespace prefix from each action's `Path` when routing (e.g. when the registry is mounted under that prefix). |
| `WithMiddleware(mw...)` | Registry-wide middleware applied to every route (actions, self-docs, `404`/`405`). |
| `WithMaxBodyBytes(n)` | Caps the request body (default 1 MiB; `0` disables). Over-limit ⇒ `413`. |
| `WithObserver(fn)` | Per-request hook with action id, status, latency, and error. |
| `WithRequestIDGenerator(fn)` | Overrides how a correlation id is minted when none is inbound (default UUIDv4). |
| `WithNotFoundHandler(h)` / `WithMethodNotAllowedHandler(h)` | Override the JSON `404` / `405` defaults. |
| `WithSecurityScheme(name, scheme)` | Declares an OpenAPI security scheme (`BearerAuth`/`APIKeyAuth` helpers). |
| `WithSecurity(reqs...)` | Sets registry-wide default security requirements. |
| `WithServers(servers...)` | Sets the OpenAPI `servers[]` block. |
| `WithOpenAPIVersion(v)` | Declares the dialect: `"3.1.0"` (default) or `"3.0.3"`. |

Production middleware & safety

Every registry ships a built-in middleware chain wrapping the typed pipeline — all on by
default and overridable:

- **Panic recovery** — a panicking handler becomes a logged, redacted `500` (and a non-nil
`Observation.Err`); the connection is never torn down.
- **Request-body cap** — bodies over `WithMaxBodyBytes` (default 1 MiB) are rejected with
`413`. Pass `0` to disable.
- **Request-id propagation** — an inbound `X-Request-ID` / `X-Amzn-Request-Id` is reused,
otherwise one is generated; it is placed in the context (`actions.RequestIDFromContext`),
echoed on the response, and included in every error envelope.
- **JSON `404` / `405`** — unmatched routes and wrong methods return the standard error
envelope, not chi's plain text.

Per-action knobs live on the `Action` struct:

```go
actions.Action[Req, Resp]{
// ...
Middleware: []actions.Middleware{authOnly}, // wraps just this route
Timeout: 2 * time.Second, // ctx deadline → 504 on overrun
Security: []actions.SecurityRequirement{{"BearerAuth": {"admin"}}},
Deprecated: true, // marked deprecated in OpenAPI
}
```

Wire observability with one hook:

```go
reg := actions.NewRegistry(actions.WithObserver(func(o actions.Observation) {
slog.Info("request",
"action", o.ActionID, "status", o.Status, "request_id", o.RequestID,
"ms", o.Duration.Milliseconds(), "err", o.Err)
}))
```

Auth & OpenAPI security

Declare security schemes once; reference them registry-wide or per action. They surface in
the generated contract under `components.securitySchemes` and `security`:

```go
reg := actions.NewRegistry(
actions.WithSecurityScheme("BearerAuth", actions.BearerAuth("JWT")),
actions.WithSecurityScheme("ApiKeyAuth", actions.APIKeyAuth("header", "X-API-Key")),
actions.WithSecurity(actions.SecurityRequirement{"ApiKeyAuth": nil}), // default for all ops
actions.WithServers(actions.Server{URL: "https://api.example.com", Description: "production"}),
)
```

go-actions does not authenticate requests itself — wire your auth as `WithMiddleware` or a
per-action `Middleware`; the security blocks document *how* clients authenticate so SDK
generators and gateways configure it correctly.

Validation tags

The `validate` struct tag drives **both** request validation and the generated JSON
Schema constraints. Supported rules:

`required`, `min=N`, `max=N`, `oneof=a b c`, `uuid`, `email`, `e164`, `rfc3339`.

For strings and slices, `min`/`max` bound the length; for numbers they bound the value.
Format rules (`uuid`, `email`, etc.) are skipped on an empty value — use `required` to
reject emptiness.

For rules a tag can't express, a request type may implement `Validatable`
(`Validate() error`); it runs after the tag rules and its field details merge into the
`422` response.


## 📚 Documentation

- **API Reference** – Dive into the godocs at [pkg.go.dev/github.com/mrz1836/go-actions](https://pkg.go.dev/github.com/mrz1836/go-actions)
- **Benchmarks** – Check the latest numbers in the [benchmarks](#-benchmarks) section
- **Test Suite** – Review the [unit tests](action_test.go) (powered by [`testify`](https://github.com/stretchr/testify))
- **Examples** – Browse the runnable pet-store API in [`examples/`](examples)


Repository Features

This repository includes 25+ built-in features covering CI/CD, security, code quality, developer experience, and community tooling.

**[View the full Repository Features list →](.github/docs/repository-features.md)**

Library Deployment

This project uses [goreleaser](https://github.com/goreleaser/goreleaser) for streamlined binary and library deployment to GitHub. To get started, install it via:

```bash
brew install goreleaser
```

The release process is defined in the [.goreleaser.yml](.goreleaser.yml) configuration file.

Then create and push a new Git tag using:

```bash
magex version:bump push=true bump=patch branch=master
```

This process ensures consistent, repeatable releases with properly versioned artifacts and metadata.

Pre-commit Hooks

Set up the Go-Pre-commit System to run the same formatting, linting, and tests defined in [AGENTS.md](.github/AGENTS.md) before every commit:

```bash
go install github.com/mrz1836/go-pre-commit/cmd/go-pre-commit@latest
go-pre-commit install
```

The system is configured via modular env files in [`.github/env/`](.github/env/README.md) and provides 17x faster execution than traditional Python-based pre-commit hooks. See the [complete documentation](http://github.com/mrz1836/go-pre-commit) for details.

GitHub Workflows

All workflows are driven by modular configuration in [`.github/env/`](.github/env/README.md) — no YAML editing required.

**[View all workflows and the control center →](.github/docs/workflows.md)**

Updating Dependencies

To update all dependencies (Go modules, linters, and related tools), run:

```bash
magex deps:update
```

This command ensures all dependencies are brought up to date in a single step, including Go modules and any tools managed by [MAGE-X](https://github.com/mrz1836/mage-x). It is the recommended way to keep your development environment and CI in sync with the latest versions.

Build Commands

View all build commands

```bash script
magex help
```


## 🧪 Examples & Tests

All unit tests run via [GitHub Actions](https://github.com/mrz1836/go-actions/actions) and use [Go version 1.25.x](https://go.dev/doc/go1.25). View the [configuration file](.github/workflows/fortress.yml).

The [`examples/`](examples) directory contains a runnable pet-store API:

- [`examples/main.go`](examples/main.go) — a server declaring actions, mounting `Handler()`, and serving `/openapi.json`, `/openapi.yaml`, and `/_actions`.
- [`examples/openapi-snapshot`](examples/openapi-snapshot) — a contract-drift guard that writes or `check`s a committed OpenAPI snapshot (wire `check` into CI to fail the build when the generated contract drifts).
- [`examples/petstore`](examples/petstore) — the shared registry both examples use.

Run the example server:

```bash script
cd examples && go run .
# then browse http://localhost:8080/_actions
```

The `actiontest` helper exercises actions through the real pipeline or directly:

```go
import "github.com/mrz1836/go-actions/actiontest"

// Spin up a test server running the full decode/validate/encode pipeline:
srv := actiontest.NewServer(t, reg)

// Or invoke a handler directly, bypassing decode/validate/encode:
resp, err := actiontest.Invoke(t, createUser(), createUserReq{Name: "Ada", Email: "ada@example.com"})
```

Run all tests (fast):

```bash script
magex test
```

Run all tests with race detector (slower):
```bash script
magex test:race
```


## 📊 Benchmarks

Run the Go benchmarks:

```bash script
magex bench
```

> Benchmarks cover the hot path — request decoding, validation, and response encoding through the typed pipeline — plus the one-time OpenAPI contract generation.


## 🛠️ Code Standards
Read more about this Go project's [code standards](.github/CODE_STANDARDS.md).


## 🤖 AI Usage & Assistant Guidelines
Read the [AI Usage & Assistant Guidelines](.github/tech-conventions/ai-compliance.md) for details on how AI is used in this project and how to interact with the AI assistants.


## 👥 Maintainers
| [MrZ](https://github.com/mrz1836) |
|:-----------------------------------------------------------------------------------------------------------:|
| [MrZ](https://github.com/mrz1836) |


## 🤝 Contributing
View the [contributing guidelines](.github/CONTRIBUTING.md) and please follow the [code of conduct](.github/CODE_OF_CONDUCT.md).

### How can I help?
All kinds of contributions are welcome :raised_hands:!
The most basic way to show your support is to star :star2: the project, or to raise issues :speech_balloon:.
You can also support this project by [becoming a sponsor on GitHub](https://github.com/sponsors/mrz1836) :clap:
or by making a [**bitcoin donation**](https://mrz1818.com/?tab=tips&utm_source=github&utm_medium=sponsor-link&utm_campaign=go-actions&utm_term=go-actions&utm_content=go-actions) to ensure this journey continues indefinitely! :rocket:

[![Stars](https://img.shields.io/github/stars/mrz1836/go-actions?label=Please%20like%20us&style=social&v=1)](https://github.com/mrz1836/go-actions/stargazers)


## 📝 License

[![License](https://img.shields.io/github/license/mrz1836/go-actions.svg?style=flat&v=1)](LICENSE)