Documentation
¶
Overview ¶
Package trpcgo is a Go-first tRPC framework that lets you define procedures in Go and automatically generates TypeScript types for @trpc/client.
Write Go structs and handlers, run [trpcgo generate], and get a fully typed TypeScript AppRouter — no manual type definitions needed.
Quick Start ¶
Define a router with procedures:
type GetUserInput struct {
ID int `json:"id"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
r := trpcgo.NewRouter()
trpcgo.Query(r, "user.get", func(ctx context.Context, input GetUserInput) (User, error) {
return User{ID: input.ID, Name: "Alice"}, nil
})
http.Handle("/trpc/", r.Handler("/trpc"))
Procedures ¶
Six registration functions cover all tRPC procedure types:
- Query and VoidQuery for read operations (GET)
- Mutation and VoidMutation for write operations (POST)
- Subscribe and VoidSubscribe for real-time streams (SSE)
Void variants are for procedures that take no input.
Router Options ¶
Configure the router with functional options:
- WithBatching — enable/disable batch request support
- WithValidator — input validation (e.g. go-playground/validator)
- WithDev — development mode with stack traces
- WithErrorFormatter — custom error response shapes
- WithContextCreator — custom context per request
- WithTypeOutput — automatic TypeScript generation at startup
- WithZodOutput — Zod schema generation alongside types
Middleware ¶
Global middleware applies to all procedures via Router.Use. Per-procedure middleware is set with the Use procedure option. Compose multiple middleware with Chain.
Error Handling ¶
Return Error values from handlers with JSON-RPC 2.0 error codes. Use NewError, NewErrorf, or WrapError to create errors. All standard tRPC error codes are provided as constants (e.g. CodeNotFound, CodeUnauthorized).
Merging Routers ¶
Combine procedures from multiple routers with MergeRouters.
Server-Side Calls ¶
Invoke procedures from Go without HTTP using Call (typed) or Router.RawCall (untyped). Both run the full middleware chain.
Code Generation ¶
The trpcgo CLI generates TypeScript types from Go source:
//go:generate trpcgo generate -output ../web/gen/trpc.ts
Install the CLI with:
go get -tool github.com/befabri/trpcgo/cmd/trpcgo
Index ¶
- func Call[I any, O any](r *Router, ctx context.Context, path string, input I) (O, error)
- func GetResponseCookies(ctx context.Context) []*http.Cookie
- func GetResponseHeaders(ctx context.Context) http.Header
- func HTTPStatusFromCode(code ErrorCode) int
- func Mutation[I any, O any](r *Router, path string, fn func(ctx context.Context, input I) (O, error), ...)
- func NameFromCode(code ErrorCode) string
- func Query[I any, O any](r *Router, path string, fn func(ctx context.Context, input I) (O, error), ...)
- func SetCookie(ctx context.Context, c *http.Cookie)
- func SetResponseHeader(ctx context.Context, key, value string)
- func Subscribe[I any, O any](r *Router, path string, ...)
- func VoidMutation[O any](r *Router, path string, fn func(ctx context.Context) (O, error), ...)
- func VoidQuery[O any](r *Router, path string, fn func(ctx context.Context) (O, error), ...)
- func VoidSubscribe[O any](r *Router, path string, fn func(ctx context.Context) (<-chan O, error), ...)
- func WithResponseMetadata(ctx context.Context) context.Context
- type Error
- type ErrorCode
- type ErrorFormatterInput
- type HandlerFunc
- type Middleware
- type Option
- func WithBatching(enabled bool) Option
- func WithContextCreator(fn func(r *http.Request) context.Context) Option
- func WithDev(enabled bool) Option
- func WithErrorFormatter(fn func(ErrorFormatterInput) any) Option
- func WithMaxBatchSize(n int) Option
- func WithMaxBodySize(n int64) Option
- func WithMethodOverride(enabled bool) Option
- func WithOnError(fn func(ctx context.Context, err *Error, path string)) Option
- func WithSSEMaxDuration(d time.Duration) Option
- func WithSSEPingInterval(d time.Duration) Option
- func WithSSEReconnectAfterInactivity(d time.Duration) Option
- func WithStrictInput(enabled bool) Option
- func WithTypeOutput(path string) Option
- func WithValidator(fn func(any) error) Option
- func WithZodMini(enabled bool) Option
- func WithZodOutput(path string) Option
- type ProcedureMeta
- type ProcedureOption
- type ProcedureType
- type Router
- func (r *Router) GenerateTS(outputPath string) error
- func (r *Router) GenerateZod(outputPath string) error
- func (r *Router) Handler(basePath string) http.Handler
- func (r *Router) Merge(sources ...*Router)
- func (r *Router) RawCall(ctx context.Context, path string, input json.RawMessage) (any, error)
- func (r *Router) Use(mw ...Middleware)
- type TrackedEvent
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Call ¶
Call invokes a typed procedure by path, running the full middleware chain. Input is marshaled to JSON and the result is unmarshaled to the output type.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/befabri/trpcgo"
)
func main() {
r := trpcgo.NewRouter()
type GreetInput struct {
Name string `json:"name"`
}
trpcgo.Query(r, "greet", func(ctx context.Context, input GreetInput) (string, error) {
return "Hello, " + input.Name + "!", nil
})
// Call invokes a procedure from Go with full type safety.
msg, err := trpcgo.Call[GreetInput, string](r, context.Background(), "greet", GreetInput{Name: "World"})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg)
}
Output: Hello, World!
func GetResponseCookies ¶
GetResponseCookies returns the cookies collected in the context by SetCookie. This is useful for RawCall callers that need to inspect cookies set by handlers. Returns nil if the context does not carry response metadata.
func GetResponseHeaders ¶
GetResponseHeaders returns the headers collected in the context by SetResponseHeader. This is useful for RawCall callers that need to inspect headers set by handlers. Returns nil if the context does not carry response metadata.
func HTTPStatusFromCode ¶
HTTPStatusFromCode returns the HTTP status code for a tRPC error code.
Example ¶
package main
import (
"fmt"
"github.com/befabri/trpcgo"
)
func main() {
status := trpcgo.HTTPStatusFromCode(trpcgo.CodeNotFound)
fmt.Println(status)
}
Output: 404
func Mutation ¶
func Mutation[I any, O any](r *Router, path string, fn func(ctx context.Context, input I) (O, error), opts ...ProcedureOption)
Mutation registers a mutation procedure.
Example ¶
r := trpcgo.NewRouter()
trpcgo.Mutation(r, "user.create", func(ctx context.Context, input CreateUserInput) (User, error) {
return User{ID: "1", Name: input.Name}, nil
})
user, err := trpcgo.Call[CreateUserInput, User](r, context.Background(), "user.create", CreateUserInput{Name: "Bob"})
if err != nil {
log.Fatal(err)
}
fmt.Println(user.Name)
Output: Bob
func NameFromCode ¶
NameFromCode returns the string name for a tRPC error code (e.g. "NOT_FOUND").
func Query ¶
func Query[I any, O any](r *Router, path string, fn func(ctx context.Context, input I) (O, error), opts ...ProcedureOption)
Query registers a query procedure.
Example ¶
r := trpcgo.NewRouter()
trpcgo.Query(r, "user.get", func(ctx context.Context, input GetUserInput) (User, error) {
return User{ID: input.ID, Name: "Alice"}, nil
})
// Call the procedure from Go (no HTTP needed).
user, err := trpcgo.Call[GetUserInput, User](r, context.Background(), "user.get", GetUserInput{ID: "1"})
if err != nil {
log.Fatal(err)
}
fmt.Println(user.Name)
Output: Alice
func SetCookie ¶
SetCookie adds a cookie to be set on the HTTP response. Call this from within a procedure handler or middleware. If the context does not carry response metadata (e.g. called outside the HTTP handler), this is a no-op. Safe for concurrent use from JSONL batch handlers.
func SetResponseHeader ¶
SetResponseHeader adds a header value to be set on the HTTP response. If the context does not carry response metadata, this is a no-op. Safe for concurrent use from JSONL batch handlers.
func Subscribe ¶
func Subscribe[I any, O any](r *Router, path string, fn func(ctx context.Context, input I) (<-chan O, error), opts ...ProcedureOption)
Subscribe registers a subscription procedure.
Example ¶
package main
import (
"context"
"fmt"
"github.com/befabri/trpcgo"
)
func main() {
r := trpcgo.NewRouter()
type EventInput struct {
Topic string `json:"topic"`
}
trpcgo.Subscribe(r, "events", func(ctx context.Context, input EventInput) (<-chan string, error) {
ch := make(chan string)
// In production, send events on ch from a goroutine and close when done.
return ch, nil
})
fmt.Println("subscription registered")
}
Output: subscription registered
func VoidMutation ¶
func VoidMutation[O any](r *Router, path string, fn func(ctx context.Context) (O, error), opts ...ProcedureOption)
VoidMutation registers a mutation procedure with no input.
func VoidQuery ¶
func VoidQuery[O any](r *Router, path string, fn func(ctx context.Context) (O, error), opts ...ProcedureOption)
VoidQuery registers a query procedure with no input.
Example ¶
r := trpcgo.NewRouter()
trpcgo.VoidQuery(r, "user.list", func(ctx context.Context) ([]User, error) {
return []User{{ID: "1", Name: "Alice"}, {ID: "2", Name: "Bob"}}, nil
})
users, err := trpcgo.Call[any, []User](r, context.Background(), "user.list", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(len(users))
Output: 2
func VoidSubscribe ¶
func VoidSubscribe[O any](r *Router, path string, fn func(ctx context.Context) (<-chan O, error), opts ...ProcedureOption)
VoidSubscribe registers a subscription procedure with no input.
func WithResponseMetadata ¶
WithResponseMetadata injects a fresh responseMetadata into the context. This is called automatically by the HTTP handler. For RawCall, callers should call this before RawCall if they need to access cookies/headers set by handlers via GetResponseCookies/GetResponseHeaders.
Types ¶
type Error ¶
Error represents a tRPC error with a JSON-RPC 2.0 error code.
func NewError ¶
NewError creates a new tRPC error.
Example ¶
package main
import (
"fmt"
"github.com/befabri/trpcgo"
)
func main() {
err := trpcgo.NewError(trpcgo.CodeNotFound, "user not found")
fmt.Println(err)
}
Output: trpc error NOT_FOUND: user not found
func WrapError ¶
WrapError creates a new tRPC error wrapping a cause.
Example ¶
package main
import (
"fmt"
"github.com/befabri/trpcgo"
)
func main() {
cause := fmt.Errorf("connection refused")
err := trpcgo.WrapError(trpcgo.CodeInternalServerError, "database error", cause)
fmt.Println(err)
}
Output: trpc error INTERNAL_SERVER_ERROR: database error: connection refused
type ErrorCode ¶
type ErrorCode int
ErrorCode represents JSON-RPC 2.0 error codes used by the tRPC wire protocol.
const ( CodeParseError ErrorCode = -32700 CodeBadRequest ErrorCode = -32600 CodeInternalServerError ErrorCode = -32603 CodeForbidden ErrorCode = -32003 CodeNotFound ErrorCode = -32004 CodeMethodNotSupported ErrorCode = -32005 CodeTimeout ErrorCode = -32008 CodeConflict ErrorCode = -32009 CodePreconditionFailed ErrorCode = -32012 CodePayloadTooLarge ErrorCode = -32013 CodeUnsupportedMedia ErrorCode = -32015 CodeUnprocessableContent ErrorCode = -32022 CodePreconditionRequired ErrorCode = -32028 CodeTooManyRequests ErrorCode = -32029 CodeClientClosed ErrorCode = -32099 CodeNotImplemented ErrorCode = -32501 CodeBadGateway ErrorCode = -32502 CodeGatewayTimeout ErrorCode = -32504 )
type ErrorFormatterInput ¶
type ErrorFormatterInput struct {
Error *Error
Type ProcedureType
Path string
Ctx context.Context
Shape errorEnvelope // the default tRPC error shape
}
ErrorFormatterInput is passed to a custom error formatter. It includes the default error shape so the formatter can extend or replace it.
type HandlerFunc ¶
HandlerFunc is the procedure handler signature. The input parameter is the already-decoded struct (or nil for void procedures). Middleware receives the same decoded input — no json.RawMessage at any layer.
type Middleware ¶
type Middleware func(next HandlerFunc) HandlerFunc
Middleware wraps a procedure handler, enabling cross-cutting concerns like logging, authentication, and error handling.
func Chain ¶
func Chain(mws ...Middleware) Middleware
Chain composes multiple middleware into one, applied left-to-right.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/befabri/trpcgo"
)
func main() {
r := trpcgo.NewRouter()
logger := func(next trpcgo.HandlerFunc) trpcgo.HandlerFunc {
return func(ctx context.Context, input any) (any, error) {
fmt.Println("log")
return next(ctx, input)
}
}
timer := func(next trpcgo.HandlerFunc) trpcgo.HandlerFunc {
return func(ctx context.Context, input any) (any, error) {
fmt.Println("time")
return next(ctx, input)
}
}
// Chain composes middleware left-to-right.
r.Use(trpcgo.Chain(logger, timer))
trpcgo.VoidQuery(r, "ping", func(ctx context.Context) (string, error) {
return "pong", nil
})
result, err := trpcgo.Call[any, string](r, context.Background(), "ping", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
}
Output: log time pong
type Option ¶
type Option func(*routerOptions)
Option configures a Router.
func WithBatching ¶
WithBatching enables or disables batch request support.
func WithContextCreator ¶
WithContextCreator sets a function that creates the base context for each request.
func WithDev ¶
WithDev enables development mode. When true, error responses include Go stack traces in the data.stack field, matching tRPC's isDev behavior.
func WithErrorFormatter ¶
func WithErrorFormatter(fn func(ErrorFormatterInput) any) Option
WithErrorFormatter sets a custom error formatter that transforms error responses. The function receives the default error shape and can return a modified or entirely different shape. This matches tRPC's errorFormatter.
func WithMaxBatchSize ¶
WithMaxBatchSize sets the maximum number of procedures allowed in a single batch request. Default is 10. Set to -1 for no limit. Passing 0 keeps the default.
func WithMaxBodySize ¶
WithMaxBodySize sets the maximum allowed request body size in bytes. Default is 1 MB. Set to -1 for no limit. Passing 0 keeps the default.
func WithMethodOverride ¶
WithMethodOverride allows clients to override HTTP method (send queries as POST).
func WithOnError ¶
WithOnError sets a callback invoked when a procedure returns an error.
func WithSSEMaxDuration ¶
WithSSEMaxDuration sets the maximum duration for SSE subscriptions. After this duration the server sends a "return" event and closes the connection; the tRPC client will automatically reconnect. Default is 0 (unlimited). In production, set a finite duration or use external connection limits to prevent resource exhaustion from clients holding connections open indefinitely.
func WithSSEPingInterval ¶
WithSSEPingInterval sets the keep-alive ping interval for SSE subscriptions. Default is 10 seconds.
func WithSSEReconnectAfterInactivity ¶
WithSSEReconnectAfterInactivity tells the client to reconnect after the given duration of inactivity. This is sent in the SSE connected event as reconnectAfterInactivityMs, matching tRPC's protocol. Default is 0 (disabled).
func WithStrictInput ¶ added in v0.3.0
WithStrictInput enables strict JSON input parsing. When true, procedure inputs that contain unknown fields (fields not present in the input struct) are rejected with a BAD_REQUEST error. This uses json.Decoder's DisallowUnknownFields under the hood.
By default, Go's json.Unmarshal silently ignores unknown fields.
func WithTypeOutput ¶
WithTypeOutput enables automatic TypeScript type generation. When set, calling Router.Handler() writes the TypeScript AppRouter type file to the given path. Use with the top-level registration functions (Query, Mutation, Subscribe, etc.) to capture type info.
func WithValidator ¶
WithValidator sets a function that validates procedure inputs. The function is called with the deserialized input struct after JSON unmarshaling. Only struct-typed inputs are validated; primitives are skipped.
This matches go-playground/validator directly — pass validate.V.Struct:
router := trpcgo.NewRouter(trpcgo.WithValidator(validate.V.Struct))
func WithZodMini ¶
WithZodMini switches Zod schema output to zod/mini functional syntax. Only has effect when WithZodOutput is also set.
func WithZodOutput ¶
WithZodOutput enables automatic Zod schema generation alongside TypeScript types. Requires WithTypeOutput to be set. The file watcher regenerates both files when Go source changes are detected.
type ProcedureMeta ¶
type ProcedureMeta struct {
Path string
Type ProcedureType
Meta any // user-defined metadata from WithMeta()
}
ProcedureMeta contains procedure metadata available to middleware via context. Use GetProcedureMeta(ctx) to read it inside middleware.
func GetProcedureMeta ¶
func GetProcedureMeta(ctx context.Context) (ProcedureMeta, bool)
GetProcedureMeta returns the procedure metadata from the context. Returns false if not available (e.g., outside a procedure call).
type ProcedureOption ¶
type ProcedureOption func(*procedureConfig)
ProcedureOption configures a single procedure registration.
func WithMeta ¶
func WithMeta(meta any) ProcedureOption
WithMeta attaches metadata to a procedure, accessible in middleware via GetProcedureMeta(ctx).
type ProcedureType ¶
type ProcedureType string
ProcedureType distinguishes queries, mutations, and subscriptions.
const ( ProcedureQuery ProcedureType = "query" ProcedureMutation ProcedureType = "mutation" ProcedureSubscription ProcedureType = "subscription" )
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router holds registered procedures and produces an http.Handler implementing the tRPC HTTP wire protocol.
func MergeRouters ¶
MergeRouters creates a new Router combining procedures from all sources. Panics if any two routers define a procedure at the same path. The returned router has default options and no global middleware.
Example ¶
users := trpcgo.NewRouter()
trpcgo.VoidQuery(users, "user.list", func(ctx context.Context) ([]User, error) {
return nil, nil
})
posts := trpcgo.NewRouter()
trpcgo.VoidQuery(posts, "post.list", func(ctx context.Context) ([]string, error) {
return nil, nil
})
// Merge combines procedures from multiple routers.
app := trpcgo.MergeRouters(users, posts)
_ = app.Handler("/trpc")
fmt.Println("merged")
Output: merged
func NewRouter ¶
NewRouter creates a new Router with the given options.
Example ¶
r := trpcgo.NewRouter(
trpcgo.WithBatching(true),
trpcgo.WithDev(true),
)
trpcgo.Query(r, "user.get", func(ctx context.Context, input GetUserInput) (User, error) {
return User{ID: input.ID, Name: "Alice"}, nil
})
fmt.Println("router created")
Output: router created
func (*Router) GenerateTS ¶
GenerateTS writes TypeScript type definitions for all registered procedures. Procedures must be registered via the top-level functions (Query, Mutation, etc.) to have type information available.
func (*Router) GenerateZod ¶ added in v0.4.0
GenerateZod writes Zod validation schemas for all registered procedure input types. Uses the same reflect-based type information as GenerateTS, enriched with Go kind and validate tag metadata.
If no procedures have typed inputs (all void), no file is written and nil is returned. Use WithZodMini to switch to zod/mini functional syntax.
func (*Router) Handler ¶
Handler returns an http.Handler that serves all registered procedures. basePath is stripped from incoming request URLs before procedure lookup.
If WithTypeOutput was configured, the TypeScript type file is written and a file watcher is started to regenerate types when Go source changes.
func (*Router) Merge ¶
Merge copies all procedures from the source routers into this router. Panics if any procedure path already exists. Global middleware and options on source routers are NOT copied.
func (*Router) RawCall ¶
RawCall invokes a procedure by path, running the full middleware chain. This is the server-side equivalent of an HTTP call — no network involved.
Subscriptions are not supported via RawCall; use the subscription handler directly.
func (*Router) Use ¶
func (r *Router) Use(mw ...Middleware)
Use adds global middleware that applies to all procedures.
Example ¶
package main
import (
"context"
"fmt"
"log"
"github.com/befabri/trpcgo"
)
func main() {
r := trpcgo.NewRouter()
// Add a logging middleware to all procedures.
r.Use(func(next trpcgo.HandlerFunc) trpcgo.HandlerFunc {
return func(ctx context.Context, input any) (any, error) {
meta, _ := trpcgo.GetProcedureMeta(ctx)
fmt.Printf("calling %s\n", meta.Path)
return next(ctx, input)
}
})
trpcgo.VoidQuery(r, "health", func(ctx context.Context) (string, error) {
return "ok", nil
})
result, err := trpcgo.Call[any, string](r, context.Background(), "health", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
}
Output: calling health ok
type TrackedEvent ¶
TrackedEvent wraps a value with an ID for SSE reconnection support. When the client disconnects and reconnects, it sends the last received ID back in the input (as lastEventId), allowing the handler to resume from where it left off.
func Tracked ¶
func Tracked[T any](id string, data T) TrackedEvent[T]
Tracked creates a TrackedEvent that associates an ID with data. The ID is sent as the SSE id field, enabling client reconnection.