virtuous

package module
v0.0.25 Latest Latest
Warning

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

Go to latest
Published: Mar 1, 2026 License: MIT Imports: 3 Imported by: 0

README

Virtuous

Virtuous is an agent-first, typed RPC API framework for Go with self-generating docs and clients.

Release Build Status Go Version License

RPC-first: APIs are plain Go functions with typed inputs and outputs, served over HTTP. Routes, schemas, docs, and JS/TS/Python clients are generated at runtime from those functions.

Compatibility: httpapi wraps existing net/http handlers when you must preserve a legacy shape or migrate gradually. New work should start with RPC.

Table of contents

Why RPC (default)

Virtuous treats APIs as typed functions instead of loosely defined HTTP resources. That keeps the surface area small, predictable, and agent-friendly.

What this means in practice:

  • Inputs/outputs are Go structs; they are the contract and generate OpenAPI + SDKs automatically.
  • Routes derive from package + function names, so naming stays consistent without manual path design.
  • A minimal handler status model (200/422/500) keeps error handling explicit and uniform.
  • Docs and clients are emitted from the running server, so they cannot drift from the code.

httpapi stays in the toolbox for teams migrating existing handlers or preserving exact legacy responses.

RPC

RPC uses plain Go functions with typed requests and responses.
Routes, schemas, and clients are inferred from package and function names.

This model minimizes surface area, avoids configuration drift, and produces predictable client code.

Quick start (cut, paste, run)
mkdir virtuous-demo
cd virtuous-demo
go mod init virtuous-demo
go get github.com/swetjen/virtuous@latest

Create main.go:

package main

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

	"github.com/swetjen/virtuous/rpc"
)

type GetStateRequest struct {
	Code string `json:"code" doc:"Two-letter state code."`
}

type State struct {
	ID   int32  `json:"id" doc:"Numeric state ID."`
	Code string `json:"code" doc:"Two-letter state code."`
	Name string `json:"name" doc:"Display name for the state."`
}

type StateResponse struct {
	State State  `json:"state"`
	Error string `json:"error,omitempty"`
}

func GetState(_ context.Context, req GetStateRequest) (StateResponse, int) {
	if req.Code == "" {
		return StateResponse{Error: "code is required"}, http.StatusUnprocessableEntity
	}
	return StateResponse{
		State: State{ID: 1, Code: req.Code, Name: "Minnesota"},
	}, http.StatusOK
}

func main() {
	router := rpc.NewRouter(rpc.WithPrefix("/rpc"))
	router.HandleRPC(GetState)
	router.ServeAllDocs()

	server := &http.Server{Addr: ":8000", Handler: router}
	fmt.Println("Listening on :8000")
	log.Fatal(server.ListenAndServe())
}

Virtuous Basic API Docs

Run it:

go run .
Advanced patterns
1) One guard for a collection of routes (group-level)

Use a dedicated router for the guarded group, then mount both routers on one mux.

admin := rpc.NewRouter(
	rpc.WithPrefix("/rpc/admin"),
	rpc.WithGuards(sessionGuard{}), // applies to every admin route
)
admin.HandleRPC(adminusers.GetMany)
admin.HandleRPC(adminusers.Disable)

public := rpc.NewRouter(rpc.WithPrefix("/rpc/public"))
public.HandleRPC(publichealth.Check)

mux := http.NewServeMux()
mux.Handle("/rpc/admin/", admin)
mux.Handle("/rpc/public/", public)

If you wrap a mux subtree with middleware directly, that works for transport security, but rpc.WithGuards(...) is the docs/client-aware path because it emits OpenAPI security metadata.

2) Multiple documentation sets (Public Service, Secret Service)

Today, one router emits one OpenAPI document for all routes on that router. For separate docs, split routes across routers.

publicAPI := rpc.NewRouter(rpc.WithPrefix("/rpc/public"))
publicAPI.HandleRPC(publicsvc.GetCatalog)
publicAPI.ServeAllDocs(
	rpc.WithDocsOptions(
		rpc.WithDocsPath("/rpc/public/docs"),
		rpc.WithOpenAPIPath("/rpc/public/openapi.json"),
	),
	rpc.WithClientJSPath("/rpc/public/client.gen.js"),
	rpc.WithClientTSPath("/rpc/public/client.gen.ts"),
	rpc.WithClientPYPath("/rpc/public/client.gen.py"),
)

secretAPI := rpc.NewRouter(
	rpc.WithPrefix("/rpc/secret"),
	rpc.WithGuards(internalTokenGuard{}),
)
secretAPI.HandleRPC(secretsvc.RotateKeys)
secretAPI.ServeAllDocs(
	rpc.WithDocsOptions(
		rpc.WithDocsPath("/rpc/secret/docs"),
		rpc.WithOpenAPIPath("/rpc/secret/openapi.json"),
	),
	rpc.WithClientJSPath("/rpc/secret/client.gen.js"),
	rpc.WithClientTSPath("/rpc/secret/client.gen.ts"),
	rpc.WithClientPYPath("/rpc/secret/client.gen.py"),
)

mux := http.NewServeMux()
mux.Handle("/rpc/public/", publicAPI)
mux.Handle("/rpc/secret/", secretAPI)
3) Basic auth on the docs route

Protect docs/OpenAPI paths at the top-level mux.

func docsBasicAuth(user, pass string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		u, p, ok := r.BasicAuth()
		if !ok || u != user || p != pass {
			// Sends a Basic Auth challenge so browsers show a username/password prompt.
			w.Header().Set("WWW-Authenticate", `Basic realm="Virtuous Docs"`)
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

router := rpc.NewRouter(rpc.WithPrefix("/rpc"))
router.HandleRPC(states.GetMany)
router.ServeAllDocs()

mux := http.NewServeMux()
mux.Handle("/rpc/", router) // API routes
mux.Handle("/rpc/openapi.json", docsBasicAuth("docs", "secret", router))
mux.Handle("/rpc/docs", docsBasicAuth("docs", "secret", router))
mux.Handle("/rpc/docs/", docsBasicAuth("docs", "secret", router))

Note: there is no first-class docs auth option yet; mux-level middleware is the current path.

4) OR auth semantics (accept either of two schemes)

When a route should accept either credential type, express that logic in one composite guard and attach it once.

type bearerOrAPIKeyGuard struct {
	bearer bearerGuard
	apiKey apiKeyGuard
}

func (g bearerOrAPIKeyGuard) Spec() guard.Spec {
	return guard.Spec{
		Name:  "BearerOrApiKey",
		In:    "header",
		Param: "Authorization",
	}
}

func (g bearerOrAPIKeyGuard) Middleware() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if g.bearer.authenticate(r) || g.apiKey.authenticate(r) {
				next.ServeHTTP(w, r)
				return
			}
			http.Error(w, "unauthorized", http.StatusUnauthorized)
		})
	}
}
Handler signature

RPC handlers must follow one of these forms:

func(context.Context, Req) (Resp, int)
func(context.Context) (Resp, int)
Status model

RPC handlers return an HTTP status code directly.

Supported statuses:

  • 200 — success
  • 422 — invalid input
  • 500 — server error

Guarded routes may also return 401 when middleware rejects the request.

Docs and SDKs are served at:

  • /rpc/docs
  • /rpc/client.gen.*
  • Responses should include a canonical error field (string or struct) when errors occur.

HTTP API (httpapi)

httpapi wraps classic net/http handlers and preserves existing request/response shapes. It also emits OpenAPI 3.0 specs for typed handlers.

Use this when:

  • Migrating an existing API to Virtuous
  • Developing rich HTTP APIs
  • Maintaining compatibility with established OpenAPI contracts
Quick start

Method-prefixed patterns (GET /path) are required for docs and client generation. Typed httpapi routes are JSON-focused for generated docs/clients. string and []byte responses are supported directly, and HandlerMeta.Responses can declare custom media types and multiple statuses.

router := httpapi.NewRouter()
router.HandleTyped(
	"GET /api/v1/lookup/states/{code}",
	httpapi.WrapFunc(StateByCode, nil, StateResponse{}, httpapi.HandlerMeta{
		Service: "States",
		Method:  "GetByCode",
	}),
)
router.ServeAllDocs()
Advanced patterns
1) One guard for a collection of routes (group-level intent)

httpapi does not have a router-wide WithGuards(...) option today. Use a shared guard slice and pass it to each route in the collection.

adminGuards := []httpapi.Guard{sessionGuard{}}

router := httpapi.NewRouter()
router.HandleTyped(
	"GET /api/admin/users",
	httpapi.WrapFunc(AdminUsersGetMany, nil, UsersResponse{}, httpapi.HandlerMeta{
		Service: "AdminUsers",
		Method:  "GetMany",
	}),
	adminGuards...,
)
router.HandleTyped(
	"POST /api/admin/users/disable",
	httpapi.WrapFunc(AdminUsersDisable, nil, DisableUserResponse{}, httpapi.HandlerMeta{
		Service: "AdminUsers",
		Method:  "Disable",
	}),
	adminGuards...,
)

If you apply middleware only at mux level, requests are still protected, but auth metadata is not emitted in OpenAPI unless guards are attached to typed routes.

2) Multiple documentation sets (Public Service, Secret Service)

Use separate routers, each with its own docs/OpenAPI/client paths.

publicAPI := httpapi.NewRouter()
publicAPI.HandleTyped(
	"GET /public/health",
	httpapi.WrapFunc(PublicHealth, nil, HealthResponse{}, httpapi.HandlerMeta{
		Service: "PublicService",
		Method:  "Health",
	}),
)
publicAPI.ServeAllDocs(
	httpapi.WithDocsOptions(
		httpapi.WithDocsPath("/public/docs"),
		httpapi.WithOpenAPIPath("/public/openapi.json"),
	),
	httpapi.WithClientJSPath("/public/client.gen.js"),
	httpapi.WithClientTSPath("/public/client.gen.ts"),
	httpapi.WithClientPYPath("/public/client.gen.py"),
)

secretGuards := []httpapi.Guard{internalTokenGuard{}}
secretAPI := httpapi.NewRouter()
secretAPI.HandleTyped(
	"POST /secret/rotate-keys",
	httpapi.WrapFunc(RotateKeys, nil, RotateKeysResponse{}, httpapi.HandlerMeta{
		Service: "SecretService",
		Method:  "RotateKeys",
	}),
	secretGuards...,
)
secretAPI.ServeAllDocs(
	httpapi.WithDocsOptions(
		httpapi.WithDocsPath("/secret/docs"),
		httpapi.WithOpenAPIPath("/secret/openapi.json"),
	),
	httpapi.WithClientJSPath("/secret/client.gen.js"),
	httpapi.WithClientTSPath("/secret/client.gen.ts"),
	httpapi.WithClientPYPath("/secret/client.gen.py"),
)

mux := http.NewServeMux()
mux.Handle("/public/", publicAPI)
mux.Handle("/secret/", secretAPI)
3) Basic auth on the docs route

Protect docs/OpenAPI paths at the top-level mux.

func docsBasicAuth(user, pass string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		u, p, ok := r.BasicAuth()
		if !ok || u != user || p != pass {
			// Sends a Basic Auth challenge so browsers show a username/password prompt.
			w.Header().Set("WWW-Authenticate", `Basic realm="Virtuous Docs"`)
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

router := httpapi.NewRouter()
router.HandleTyped(
	"GET /api/v1/lookup/states/{code}",
	httpapi.WrapFunc(StateByCode, nil, StateResponse{}, httpapi.HandlerMeta{
		Service: "States",
		Method:  "GetByCode",
	}),
)
router.ServeAllDocs()

mux := http.NewServeMux()
mux.Handle("/", router) // API routes
mux.Handle("/openapi.json", docsBasicAuth("docs", "secret", router))
mux.Handle("/docs", docsBasicAuth("docs", "secret", router))
mux.Handle("/docs/", docsBasicAuth("docs", "secret", router))

Note: there is no first-class docs auth option yet; mux-level middleware is the current path.

4) OR auth semantics (accept either of two schemes)

When a route should accept either credential type, express that logic in one composite guard and attach it once.

type bearerOrAPIKeyGuard struct {
	bearer bearerGuard
	apiKey apiKeyGuard
}

func (g bearerOrAPIKeyGuard) Spec() guard.Spec {
	return guard.Spec{
		Name:  "BearerOrApiKey",
		In:    "header",
		Param: "Authorization",
	}
}

func (g bearerOrAPIKeyGuard) Middleware() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if g.bearer.authenticate(r) || g.apiKey.authenticate(r) {
				next.ServeHTTP(w, r)
				return
			}
			http.Error(w, "unauthorized", http.StatusUnauthorized)
		})
	}
}
5) Optional request body contract

Request bodies are required by default when you pass a typed request. Use httpapi.Optional when a route should accept either no body or a JSON body.

router := httpapi.NewRouter()
router.HandleTyped(
	"POST /api/v1/search",
	httpapi.WrapFunc(Search, httpapi.Optional[SearchRequest](), SearchResponse{}, httpapi.HandlerMeta{
		Service: "Search",
		Method:  "Run",
	}),
)
6) Explicit response specs

Use HandlerMeta.Responses when a route needs multiple statuses or a custom response media type.

router := httpapi.NewRouter()
router.HandleTyped(
	"GET /api/v1/assets/{id}/preview.png",
	httpapi.WrapFunc(ServePreviewPNG, nil, nil, httpapi.HandlerMeta{
		Service: "Assets",
		Method:  "GetPreview",
		Responses: []httpapi.ResponseSpec{
			{Status: 200, Body: []byte{}, MediaType: "image/png"},
			{Status: 404, Body: ErrorResponse{}},
		},
	}),
)

Combined (migration demo)

Both routers can be mounted in the same server to support incremental migration.

This layout is intended for transition periods, not as a long-term structure.

httpRouter := httpstates.BuildRouter()

rpcRouter := rpc.NewRouter(rpc.WithPrefix("/rpc"))
rpcRouter.HandleRPC(rpcusers.UsersGetMany)
rpcRouter.HandleRPC(rpcusers.UserCreate)

mux := http.NewServeMux()
mux.Handle("/rpc/", rpcRouter)
mux.Handle("/", httpRouter)

Why RPC?

Virtuous uses an RPC-style API model because it produces simpler, more reliable systems—especially when APIs are consumed by agents.

RPC treats APIs as typed functions, not as collections of loosely related HTTP resources. This keeps the surface area small and the intent explicit.

What RPC optimizes for
  • Clarity over convention — function names express intent directly, without guessing paths or schemas.
  • Types as the contract — request and response structs are the API; no separate schema to sync.
  • Predictable code generation — small, explicit signatures produce reliable client SDKs.
  • Fewer invalid states — avoids ambiguous partial updates, nested resources, and overloaded semantics.
  • Runtime truth — routes, schemas, docs, and clients all derive from the same runtime definitions.
HTTP still matters

Virtuous RPC runs on HTTP and uses HTTP status codes intentionally.
What changes is the mental model: from “resources and verbs” to “operations with inputs and outputs.”

For teams migrating existing APIs or preserving established contracts, Virtuous also supports classic net/http handlers via httpapi.

RPC is the default because it’s harder to misuse and easier to automate.

Docs

  • docs/overview.md — primary documentation (RPC-first)
  • docs/agent_quickstart.md — agent-oriented usage guide
  • example/byodb/docs/STYLEGUIDES.md — byodb styleguide index and canonical flow

Requirements

  • Go 1.25+

Documentation

Index

Constants

View Source
const (
	RPCStatusOK      = rpc.StatusOK
	RPCStatusInvalid = rpc.StatusInvalid
	RPCStatusError   = rpc.StatusError
)

Variables

This section is empty.

Functions

func Cors

func Cors(opts ...CORSOption) func(http.Handler) http.Handler

func Decode

func Decode[T any](r *http.Request) (T, error)

func DefaultDocsHTML

func DefaultDocsHTML(openAPIPath string) string

func Encode

func Encode(w http.ResponseWriter, r *http.Request, status int, v any)

func NewRPCRouter

func NewRPCRouter(opts ...rpc.RouterOption) *rpc.Router

RPC function shims.

func Optional added in v0.0.25

func Optional[T any](req ...T) any

func RPCDefaultDocsHTML

func RPCDefaultDocsHTML(openAPIPath string) string

func RPCWithGuards

func RPCWithGuards(guards ...rpc.Guard) rpc.RouterOption

func RPCWithPrefix

func RPCWithPrefix(prefix string) rpc.RouterOption

func RPCWriteDocsHTMLFile

func RPCWriteDocsHTMLFile(path, openAPIPath string) error

func WriteDocsHTMLFile

func WriteDocsHTMLFile(path, openAPIPath string) error

Types

type CORSOption

type CORSOption = httpapi.CORSOption

func WithAllowCredentials

func WithAllowCredentials(enabled bool) CORSOption

func WithAllowedHeaders

func WithAllowedHeaders(headers ...string) CORSOption

func WithAllowedMethods

func WithAllowedMethods(methods ...string) CORSOption

func WithAllowedOrigins

func WithAllowedOrigins(origins ...string) CORSOption

func WithExposedHeaders

func WithExposedHeaders(headers ...string) CORSOption

func WithMaxAgeSeconds

func WithMaxAgeSeconds(seconds int) CORSOption

type CORSOptions

type CORSOptions = httpapi.CORSOptions

type DocOpt

type DocOpt = httpapi.DocOpt

func WithDocsFile

func WithDocsFile(path string) DocOpt

func WithDocsPath

func WithDocsPath(path string) DocOpt

func WithOpenAPIFile

func WithOpenAPIFile(path string) DocOpt

func WithOpenAPIPath

func WithOpenAPIPath(path string) DocOpt

type DocsOptions

type DocsOptions = httpapi.DocsOptions

type Guard

type Guard = httpapi.Guard

Type aliases for backwards compatibility.

type GuardSpec

type GuardSpec = httpapi.GuardSpec

type HandlerMeta

type HandlerMeta = httpapi.HandlerMeta

type NoResponse200

type NoResponse200 = httpapi.NoResponse200

type NoResponse204

type NoResponse204 = httpapi.NoResponse204

type NoResponse500

type NoResponse500 = httpapi.NoResponse500

type OpenAPIContact

type OpenAPIContact = httpapi.OpenAPIContact

type OpenAPIExternalDocs

type OpenAPIExternalDocs = httpapi.OpenAPIExternalDocs

type OpenAPILicense

type OpenAPILicense = httpapi.OpenAPILicense

type OpenAPIOptions

type OpenAPIOptions = httpapi.OpenAPIOptions

type OpenAPIServer

type OpenAPIServer = httpapi.OpenAPIServer

type OpenAPITag

type OpenAPITag = httpapi.OpenAPITag

type RPCDocOpt

type RPCDocOpt = rpc.DocOpt

func RPCWithDocsFile

func RPCWithDocsFile(path string) RPCDocOpt

func RPCWithDocsPath

func RPCWithDocsPath(path string) RPCDocOpt

func RPCWithOpenAPIFile

func RPCWithOpenAPIFile(path string) RPCDocOpt

func RPCWithOpenAPIPath

func RPCWithOpenAPIPath(path string) RPCDocOpt

type RPCDocsOptions

type RPCDocsOptions = rpc.DocsOptions

type RPCGuard

type RPCGuard = rpc.Guard

RPC type aliases for convenience.

type RPCGuardSpec

type RPCGuardSpec = rpc.GuardSpec

type RPCOpenAPIContact

type RPCOpenAPIContact = rpc.OpenAPIContact

type RPCOpenAPIExternalDocs

type RPCOpenAPIExternalDocs = rpc.OpenAPIExternalDocs

type RPCOpenAPILicense

type RPCOpenAPILicense = rpc.OpenAPILicense

type RPCOpenAPIOptions

type RPCOpenAPIOptions = rpc.OpenAPIOptions

type RPCOpenAPIServer

type RPCOpenAPIServer = rpc.OpenAPIServer

type RPCOpenAPITag

type RPCOpenAPITag = rpc.OpenAPITag

type RPCRoute

type RPCRoute = rpc.Route

type RPCRouter

type RPCRouter = rpc.Router

type RPCServeAllDocsOpt

type RPCServeAllDocsOpt = rpc.ServeAllDocsOpt

func RPCWithClientJSPath

func RPCWithClientJSPath(path string) RPCServeAllDocsOpt

func RPCWithClientPYPath

func RPCWithClientPYPath(path string) RPCServeAllDocsOpt

func RPCWithClientTSPath

func RPCWithClientTSPath(path string) RPCServeAllDocsOpt

func RPCWithDocsOptions

func RPCWithDocsOptions(opts ...rpc.DocOpt) RPCServeAllDocsOpt

func RPCWithoutDocs

func RPCWithoutDocs() RPCServeAllDocsOpt

type RPCServeAllDocsOptions

type RPCServeAllDocsOptions = rpc.ServeAllDocsOptions

type RPCTypeOverride

type RPCTypeOverride = rpc.TypeOverride

type ResponseSpec added in v0.0.25

type ResponseSpec = httpapi.ResponseSpec

type Route

type Route = httpapi.Route

type Router

type Router = httpapi.Router

func NewRouter

func NewRouter() *Router

Function shims for backwards compatibility.

type ServeAllDocsOpt

type ServeAllDocsOpt = httpapi.ServeAllDocsOpt

func WithClientJSPath

func WithClientJSPath(path string) ServeAllDocsOpt

func WithClientPYPath

func WithClientPYPath(path string) ServeAllDocsOpt

func WithClientTSPath

func WithClientTSPath(path string) ServeAllDocsOpt

func WithDocsOptions

func WithDocsOptions(opts ...DocOpt) ServeAllDocsOpt

func WithoutDocs

func WithoutDocs() ServeAllDocsOpt

type ServeAllDocsOptions

type ServeAllDocsOptions = httpapi.ServeAllDocsOptions

type TypeOverride

type TypeOverride = httpapi.TypeOverride

type TypedHandler

type TypedHandler = httpapi.TypedHandler

func Wrap

func Wrap(handler http.Handler, req any, resp any, meta HandlerMeta) TypedHandler

func WrapFunc

func WrapFunc(handler func(http.ResponseWriter, *http.Request), req any, resp any, meta HandlerMeta) TypedHandler

type TypedHandlerFunc

type TypedHandlerFunc = httpapi.TypedHandlerFunc

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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