Virtuous is an agent-first, typed RPC API framework for Go with self-generating docs and clients.
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.
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 status model (200/401/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 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
- Developing rich http APIs.
- 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+
