Virtuous is an agent-first API framework for Go with self-generating docs and clients.
Virtuous supports two API styles:
- RPC — the primary, recommended model
- httpapi — compatibility support for existing
net/httphandlers
RPC is optimized for simplicity, correctness, and reliable code generation.
httpapi exists to support migration and interoperability with existing APIs.
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.
mkdir virtuous-demo
cd virtuous-demo
go mod init virtuous-demo
go get github.com/swetjen/virtuous@latestCreate 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())
}Run it:
go run .RPC handlers must follow one of these forms:
func(context.Context, Req) (Resp, int)
func(context.Context) (Resp, int)RPC handlers return an HTTP status code directly.
Supported statuses:
200— success401— unauthorized (guard)422— invalid input500— server error
Docs and SDKs are served at:
/rpc/docs/rpc/client.gen.*- Responses should include a canonical
errorfield (string or struct) when errors occur.
httpapi wraps classic net/http handlers and preserves existing request/response shapes. It also implements automatic OpenAPI 3.0 specs for all handlers wrapped in this way.
Use this when:
- Migrating an existing API to Virtuous
- Maintaining compatibility with established OpenAPI contracts
router := httpapi.NewRouter()
router.HandleTyped(
"GET /api/v1/lookup/states/{code}",
httpapi.WrapFunc(StateByCode, nil, StateResponse{}, httpapi.HandlerMeta{
Service: "States",
Method: "GetByCode",
}),
)
router.ServeAllDocs()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)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.
- 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.
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/overview.md— primary documentation (RPC-first)docs/agent_quickstart.md— agent-oriented usage guideexample/byodb/docs/STYLEGUIDES.md— byodb styleguide index and canonical flow
- Go 1.25+
