This is the full developer documentation for trpcgo. --- # trpcgo > Write tRPC-compatible APIs in Go and generate the TypeScript router contract your frontend consumes. trpcgo is for teams that want the tRPC developer experience without running the API server in TypeScript. Your Go handlers and structs are the source of truth. trpcgo serves them over the tRPC HTTP protocol and generates the TypeScript `AppRouter` type that `@trpc/client`, `@trpc/react-query`, and `@trpc/tanstack-react-query` understand. ## Why Use It ### Go API server Register typed Go queries, mutations, and SSE subscriptions on a plain `net/http` handler. ### Typed frontend Generate a structural `AppRouter` contract so TypeScript catches broken procedure calls. ### Validation schemas Generate Zod input schemas from Go `validate` tags for client-side form validation. ### Runtime controls Configure batching, strict JSON decoding, body limits, SSE limits, context, middleware, and error formatting. ### Source analysis Use `trpcgo generate` to preserve comments, const unions, aliases, generics, and validation metadata. ### Dev watch In dev mode, regenerate frontend files on Go file changes without restarting the server. ## The Basic Shape ```go router := trpcgo.NewRouter( trpcgo.WithValidator(validate.Struct), trpcgo.WithTypeOutput("../web/gen/trpc.ts"), trpcgo.WithZodOutput("../web/gen/zod.ts"), ) defer router.Close() trpcgo.MustQuery(router, "user.get", getUser) trpcgo.MustMutation(router, "user.create", createUser) mux := http.NewServeMux() mux.Handle("/trpc/", trpc.NewHandler(router, "/trpc")) ``` ```ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './gen/trpc.js'; const client = createTRPCClient({ links: [httpBatchLink({ url: '/trpc' })], }); const user = await client.user.get.query({ id: '1' }); ``` ## Where To Go Next Read [Core Concepts](/concepts/) to understand the runtime model, then follow [Quick Start](/quick-start/) to build the first endpoint. --- # Core Concepts > Understand how trpcgo connects Go handlers, the tRPC HTTP protocol, and generated TypeScript contracts. trpcgo has three main pieces: a Go procedure registry, an HTTP protocol handler, and TypeScript/Zod generation. ## Router `Router` owns registered procedures, global middleware, router options, output hooks, and the optional development file watcher. Create one router for the API surface you want to serve: ```go router := trpcgo.NewRouter( trpcgo.WithBatching(true), trpcgo.WithStrictInput(true), trpcgo.WithValidator(validate.Struct), ) defer router.Close() ``` Register all procedures and middleware before constructing the HTTP handler. `trpc.NewHandler(router, "/trpc")` snapshots the current procedure map, so later registrations do not affect that handler. ## Procedures A procedure is a typed Go function registered at a string path: ```go trpcgo.MustQuery(router, "user.get", getUser) trpcgo.MustMutation(router, "user.create", createUser) trpcgo.MustVoidSubscribe(router, "user.onCreated", onUserCreated) ``` The path becomes the tRPC procedure name. Dot-separated paths become nested TypeScript client properties, so `"user.get"` becomes `client.user.get.query(...)`. trpcgo supports: - Queries for reads. - Mutations for writes. - Subscriptions for server-sent event streams. - Void variants for procedures with no input. - `Must*` variants for startup code where duplicate paths are programmer errors. ## HTTP Handler Package `github.com/befabri/trpcgo/trpc` exposes the tRPC-compatible HTTP handler: ```go mux.Handle("/trpc/", trpc.NewHandler(router, "/trpc")) ``` If the base path is `/trpc`, a request to `/trpc/user.get` resolves to procedure `user.get`. The handler implements the important tRPC wire behavior: - `GET` queries with `?input=`. - `POST` mutations with a JSON body. - Optional query-over-POST when `WithMethodOverride(true)` is set. - JSON batch requests with `?batch=1`. - JSONL batch streaming with `trpc-accept: application/jsonl`. - SSE subscriptions using `text/event-stream`. ## Middleware And Context Middleware wraps decoded procedure input, not raw JSON. Global middleware runs before per-procedure middleware. ```go router.Use(requestTimer) trpcgo.MustMutation(router, "user.create", createUser, trpcgo.Use(requireAuth), trpcgo.WithMeta(map[string]string{"action": "write"}), ) ``` Inside middleware or handlers, use `GetProcedureMeta(ctx)` to read the active path, procedure type, and custom metadata. Use `WithContextCreator` to derive a request context from `*http.Request`, for example to attach auth claims or request IDs. ## Generation Paths There are two ways to generate TypeScript: - Static analysis: `go tool trpcgo generate` reads Go source with `go/packages`. This is the recommended production path because it sees comments, aliases, const unions, validate tags, and typed output parsers. - Runtime reflection: `router.GenerateTS(...)` and `router.GenerateZod(...)` read registered procedure reflection types. They are useful at startup but cannot see source-only metadata like Go doc comments. In development, `WithDev(true)` plus `WithTypeOutput(...)` starts a source-analysis watcher when the HTTP handler is constructed. It generates once from source, then updates generated files on Go file changes if the source still type-checks. ## Source Of Truth Go types define the API contract: ```go type CreateUserInput struct { Name string `json:"name" validate:"required,min=1,max=100"` Email string `json:"email" validate:"required,email"` } ``` trpcgo uses those types for request decoding, optional server-side validation, TypeScript generation, and optional Zod generation. The generated frontend types should be treated as build artifacts, not hand-edited source. --- # Install > Add the trpcgo runtime and generator to a Go module. trpcgo ships as a Go runtime package and a Go tool command. ## Add The Runtime ```bash go get github.com/befabri/trpcgo@latest ``` Import the runtime and HTTP protocol handler separately: ```go import ( "github.com/befabri/trpcgo" "github.com/befabri/trpcgo/trpc" ) ``` ## Add The Generator With Go 1.26+, add the generator as a tool in `go.mod`: ```go tool github.com/befabri/trpcgo/cmd/trpcgo ``` Then run it with `go tool`: ```bash mkdir -p web/gen go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./... ``` The CLI creates output files directly, so parent directories must already exist. ## Frontend Packages Install the tRPC client packages used by your frontend framework. For a vanilla client: ```bash npm install @trpc/client @trpc/server ``` For React Query: ```bash npm install @trpc/client @trpc/server @trpc/react-query @tanstack/react-query ``` For the TanStack React Query helper API shown in [Frontend Setup](/frontend-setup/): ```bash npm install @trpc/client @trpc/server @trpc/tanstack-react-query @tanstack/react-query ``` Install Zod if you generate schemas: ```bash npm install zod ``` ## Requirements - Go 1.26 or newer. - tRPC v11 client packages. - Zod 4 when using `--zod` or `WithZodOutput`. ## What trpcgo Does Not Install trpcgo does not add CORS, authentication, persistence, or a web framework. The HTTP handler is plain `net/http`, so mount it behind Chi, Echo, Fiber adapters, standard middleware, or a raw `http.ServeMux`. --- # Quick Start > Build one Go tRPC endpoint, generate TypeScript, and call it from a frontend. This guide creates a `user.create` mutation and calls it with a typed tRPC client. ## 1. Define Types And Handler ```go package main import "context" type CreateUserInput struct { Name string `json:"name" validate:"required,min=1,max=100"` Email string `json:"email" validate:"required,email"` } type User struct { ID string `json:"id" tstype:",readonly"` Name string `json:"name"` Email string `json:"email"` } func createUser(ctx context.Context, input CreateUserInput) (User, error) { return User{ID: "1", Name: input.Name, Email: input.Email}, nil } ``` ## 2. Register And Serve Procedures ```go package main import ( "log" "net/http" "github.com/befabri/trpcgo" "github.com/befabri/trpcgo/trpc" "github.com/go-playground/validator/v10" ) //go:generate go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./... func main() { validate := validator.New() router := trpcgo.NewRouter( trpcgo.WithDev(true), trpcgo.WithStrictInput(true), trpcgo.WithValidator(validate.Struct), trpcgo.WithTypeOutput("web/gen/trpc.ts"), trpcgo.WithZodOutput("web/gen/zod.ts"), ) defer router.Close() trpcgo.MustMutation(router, "user.create", createUser) mux := http.NewServeMux() mux.Handle("/trpc/", trpc.NewHandler(router, "/trpc")) log.Fatal(http.ListenAndServe(":8080", mux)) } ``` `WithValidator(validate.Struct)` is what makes `validate` tags run on the server. Without it, the tags still help Zod generation but runtime input validation is disabled. ## 3. Generate Types ```bash mkdir -p web/gen go generate ./... ``` The CLI writes output files directly and does not create missing parent directories, so create `web/gen` before the first generation run. The generated `trpc.ts` contains `AppRouter`, `RouterInputs`, `RouterOutputs`, and TypeScript definitions for reachable Go types. The generated `zod.ts` contains schemas for typed procedure inputs: ```ts import { z } from 'zod'; export const CreateUserInputSchema = z.object({ name: z.string().min(1).max(100), email: z.email(), }); ``` ## 4. Call From TypeScript ```ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './gen/trpc.js'; import { CreateUserInputSchema } from './gen/zod.js'; const client = createTRPCClient({ links: [httpBatchLink({ url: 'http://localhost:8080/trpc' })], }); const input = CreateUserInputSchema.parse({ name: 'Alice', email: 'alice@example.com', }); const user = await client.user.create.mutate(input); ``` If you change `CreateUserInput` or `User` in Go and regenerate, TypeScript call sites update immediately. ## Full Example The repository includes `examples/start-trpc/`, a Go server plus TanStack Start frontend demonstrating queries, mutations, SSE subscriptions, generated Zod schemas, middleware, error formatting, and server-side calls. --- # Procedures > Register typed Go queries, mutations, subscriptions, reusable base procedures, and output hooks. Procedures are the unit of work exposed to tRPC clients. Each procedure has a path, a type, an optional input type, an output type, middleware, metadata, and optional output hooks. ## Queries Use queries for reads. ```go type GetUserInput struct { ID string `json:"id" validate:"required"` } func getUser(ctx context.Context, input GetUserInput) (User, error) { return db.FindUser(input.ID) } trpcgo.MustQuery(router, "user.get", getUser) ``` Use `VoidQuery` when there is no input: ```go trpcgo.MustVoidQuery(router, "system.health", func(ctx context.Context) (HealthInfo, error) { return HealthInfo{OK: true}, nil }) ``` ## Mutations Use mutations for writes. ```go trpcgo.MustMutation(router, "user.create", func(ctx context.Context, input CreateUserInput) (User, error) { return db.CreateUser(input) }) ``` Use `VoidMutation` when there is no input: ```go trpcgo.MustVoidMutation(router, "system.reset", func(ctx context.Context) (ResetResult, error) { return resetDemoData() }) ``` ## Subscriptions Subscriptions return a receive-only channel and are served as SSE streams. ```go trpcgo.MustSubscribe(router, "chat.messages", func(ctx context.Context, input RoomInput) (<-chan Message, error) { ch := make(chan Message) go func() { defer close(ch) // Send values until ctx is canceled. }() return ch, nil }) ``` Use `SubscribeWithFinal` to send a final value in the SSE `return` event after the channel closes: ```go trpcgo.MustSubscribeWithFinal(router, "job.progress", func(ctx context.Context, input JobInput) (<-chan Progress, func() any, error) { ch := make(chan Progress) final := func() any { return map[string]string{"status": "done"} } return ch, final, nil }) ``` ## Error Handling At Registration Non-`Must` functions return duplicate path errors: ```go if err := trpcgo.Query(router, "user.get", getUser); err != nil { return err } ``` `Must*` variants panic and are intended for application startup code where a duplicate path is a programmer mistake. ## Procedure Options Every registration function accepts procedure options: ```go trpcgo.MustMutation(router, "user.create", createUser, trpcgo.Use(requireAuth, rateLimit), trpcgo.WithMeta(map[string]string{"action": "write"}), ) ``` Available procedure options include: | Option | Purpose | | --- | --- | | `Use(mw...)` | Adds per-procedure middleware. | | `WithMeta(meta)` | Attaches metadata readable through `GetProcedureMeta` or `GetMeta[T]`. | | `WithOutputValidator(fn)` | Validates successful outputs without changing their type. | | `OutputValidator[O](fn)` | Typed output validator. | | `WithOutputParser(fn)` | Validates or transforms output, but generated output type becomes `unknown`. | | `OutputParser[O, P](fn)` | Typed output parser; generated output type becomes `P`. | ## Base Procedures `Procedure()` builds immutable, reusable procedure options. Each chain call returns a new builder. ```go publicProcedure := trpcgo.Procedure() authedProcedure := publicProcedure.Use(requireAuth) adminProcedure := authedProcedure.Use(requireAdmin).WithMeta(RoleMeta{Role: "admin"}) trpcgo.MustQuery(router, "user.list", listUsers, authedProcedure) trpcgo.MustMutation(router, "user.create", createUser, authedProcedure) trpcgo.MustMutation(router, "admin.ban", banUser, adminProcedure) ``` Seed one builder from another when composing domains: ```go orgProcedure := trpcgo.Procedure(authedProcedure).Use(requireOrgAccess) ``` ## Output Hooks Output hooks run after a handler succeeds. Validators run before parsers. ```go trpcgo.MustQuery(router, "user.get", getUser, trpcgo.OutputValidator(func(u User) error { if u.ID == "" { return errors.New("id required") } return nil }), ) ``` Use typed parsers when the client should see a sanitized shape: ```go type PublicUser struct { ID string `json:"id"` Name string `json:"name"` } trpcgo.MustQuery(router, "user.get", getUser, trpcgo.OutputParser(func(u User) (PublicUser, error) { return PublicUser{ID: u.ID, Name: u.Name}, nil }), ) ``` For subscriptions, output validators and parsers run for every emitted item. If an output hook fails, the client receives an SSE `serialized-error` event and the stream closes. > [!CAUTION] > Untyped `WithOutputParser` changes runtime output but cannot tell codegen the resulting shape. Use `OutputParser[O, P]` when the TypeScript output type should change. --- # Router & Options > Configure the trpcgo runtime, serve procedures over HTTP, and merge routers. `Router` owns procedure registrations and runtime configuration. ```go router := trpcgo.NewRouter( trpcgo.WithBatching(true), trpcgo.WithStrictInput(true), trpcgo.WithValidator(validate.Struct), ) defer router.Close() ``` ## Serve A Router Use the `trpc` package to serve a router over the tRPC HTTP protocol: ```go mux := http.NewServeMux() mux.Handle("/trpc/", trpc.NewHandler(router, "/trpc")) ``` The base path is stripped before procedure lookup. With base path `/trpc`, `/trpc/user.get` maps to procedure `user.get`. > [!CAUTION] > `trpc.NewHandler` snapshots the router procedure map. Register procedures, merge routers, and add global middleware before constructing the handler. ## Request Options | Option | Default | Behavior | | --- | --- | --- | | `WithBatching(bool)` | `true` | Enables tRPC batch requests with `?batch=1`. | | `WithMethodOverride(bool)` | `false` | Allows queries to be called with `POST`. | | `WithMaxBodySize(n)` | `1 MiB` | Limits POST bodies and GET `input` query values. `-1` disables the limit. | | `WithMaxBatchSize(n)` | `10` | Limits procedures in one batch. `-1` disables the limit. | | `WithStrictInput(bool)` | `false` | Rejects unknown JSON object fields with `BAD_REQUEST`. | Strict input uses Go's `json.Decoder.DisallowUnknownFields`. By default, unknown JSON fields are silently ignored like normal `json.Unmarshal`. ## Validation Option `WithValidator` runs after JSON decoding and only for struct-typed inputs. ```go validate := validator.New() router := trpcgo.NewRouter( trpcgo.WithValidator(validate.Struct), ) ``` `validate` tags do not run at runtime unless you configure this option. ## Context And Error Options | Option | Behavior | | --- | --- | | `WithContextCreator(fn)` | Derives the request context from `r.Context()` and `*http.Request`. | | `WithOnError(fn)` | Receives tRPC errors for server-side logging/observability before response formatting. | | `WithErrorFormatter(fn)` | Changes the serialized error shape sent to clients. | | `WithDev(bool)` | Adds stack traces to error responses and enables dev generation behavior. | Example context creator: ```go router := trpcgo.NewRouter( trpcgo.WithContextCreator(func(ctx context.Context, r *http.Request) context.Context { if reqID := r.Header.Get("X-Request-ID"); reqID != "" { ctx = context.WithValue(ctx, requestIDKey, reqID) } return ctx }), ) ``` The returned context still cancels when the original request context cancels. ## SSE Options | Option | Default | Behavior | | --- | --- | --- | | `WithSSEPingInterval(d)` | `10s` | Sends keep-alive `ping` events. | | `WithSSEMaxDuration(d)` | `30m` | Closes streams with a `return` event after the duration. `-1` means unlimited. | | `WithSSEReconnectAfterInactivity(d)` | disabled | Sends `reconnectAfterInactivityMs` in the `connected` event. | | `WithSSEMaxConnections(n)` | unlimited | Rejects extra streams with `TOO_MANY_REQUESTS`. | ## Generation Options | Option | Behavior | | --- | --- | | `WithTypeOutput(path)` | Writes generated TypeScript in dev mode. | | `WithZodOutput(path)` | Writes generated Zod schemas in dev mode. | | `WithZodMini(bool)` | Uses `zod/mini` functional syntax. | | `WithWatchPackages(patterns...)` | Restricts dev watcher analysis to package patterns like `./cmd/api` or `./internal/...`. | Dev generation starts when `trpc.NewHandler` is constructed and `WithDev(true)` plus `WithTypeOutput(...)` are set. ## Router Merging Use router merging to split registrations across packages. ```go userRouter := trpcgo.NewRouter() trpcgo.MustQuery(userRouter, "user.list", listUsers) adminRouter := trpcgo.NewRouter() trpcgo.MustMutation(adminRouter, "admin.ban", banUser) apiRouter := trpcgo.NewRouter() if err := apiRouter.Merge(userRouter, adminRouter); err != nil { log.Fatal(err) } ``` `Merge` is atomic: if any duplicate path is found, no procedures are copied. Source router options and global middleware are not copied, only procedures with their per-procedure middleware, metadata, and output hooks. `MergeRouters(...)` creates a new router with default options and no global middleware. --- # Middleware & Metadata > Wrap procedure handlers, attach metadata, and derive request context. Middleware has this shape: ```go type Middleware func(next trpcgo.HandlerFunc) trpcgo.HandlerFunc ``` `HandlerFunc` receives already-decoded input. Middleware does not receive raw JSON. ## Global Middleware Global middleware applies to every procedure on a router. ```go router.Use(func(next trpcgo.HandlerFunc) trpcgo.HandlerFunc { return func(ctx context.Context, input any) (any, error) { meta, _ := trpcgo.GetProcedureMeta(ctx) start := time.Now() result, err := next(ctx, input) log.Printf("[%s] %s took %s", meta.Type, meta.Path, time.Since(start)) return result, err } }) ``` ## Per-Procedure Middleware Use `trpcgo.Use(...)` on a single procedure or a base procedure builder. ```go trpcgo.MustMutation(router, "user.create", createUser, trpcgo.Use(requireAuth, rateLimit), ) ``` Global middleware wraps per-procedure middleware, so the call order is global middleware first, then per-procedure middleware, then the handler. ## Procedure Metadata Attach arbitrary metadata with `WithMeta`: ```go type RouteMeta struct { AuthRequired bool AuditAction string } trpcgo.MustMutation(router, "user.create", createUser, trpcgo.WithMeta(RouteMeta{AuthRequired: true, AuditAction: "user.create"}), ) ``` Read metadata in middleware: ```go func requireAuth(next trpcgo.HandlerFunc) trpcgo.HandlerFunc { return func(ctx context.Context, input any) (any, error) { meta, _ := trpcgo.GetProcedureMeta(ctx) routeMeta, _ := trpcgo.GetMeta[RouteMeta](ctx) if routeMeta.AuthRequired && ctx.Value(userKey) == nil { return nil, trpcgo.NewError(trpcgo.CodeUnauthorized, "login required") } log.Printf("%s %s", meta.Type, meta.Path) return next(ctx, input) } } ``` `ProcedureMeta` contains: | Field | Meaning | | --- | --- | | `Path` | Registered procedure path, such as `user.create`. | | `Type` | `query`, `mutation`, or `subscription`. | | `Meta` | The value passed to `WithMeta`. | ## Request Context Use `WithContextCreator` to derive the context passed to procedures from the incoming HTTP request. ```go router := trpcgo.NewRouter( trpcgo.WithContextCreator(func(ctx context.Context, r *http.Request) context.Context { token := r.Header.Get("Authorization") if token != "" { ctx = context.WithValue(ctx, authTokenKey, token) } return ctx }), ) ``` Cancellation still follows the original request context. If either the original request context or the returned context is canceled, procedure execution sees cancellation. ## Response Headers And Cookies Handlers and middleware can set HTTP response metadata through the context: ```go trpcgo.SetResponseHeader(ctx, "X-Trace-ID", traceID) trpcgo.SetCookie(ctx, &http.Cookie{Name: "session", Value: sessionID, Path: "/"}) ``` See [Response Metadata](/response-metadata/) for details and `RawCall` behavior. --- # HTTP Protocol > How the trpcgo HTTP handler maps tRPC requests to Go procedures. The `trpc` package implements the tRPC HTTP wire format on top of `net/http`. ## Mounting ```go mux.Handle("/trpc/", trpc.NewHandler(router, "/trpc")) ``` The base path is stripped before procedure lookup: | URL | Procedure | | --- | --- | | `/trpc/user.get` | `user.get` | | `/trpc/admin.audit.list` | `admin.audit.list` | Path traversal segments `.` and `..` are rejected. ## Methods | Procedure type | Method | | --- | --- | | Query | `GET` by default. `POST` only with `WithMethodOverride(true)`. | | Mutation | `POST`. | | Subscription | `GET` or `POST`, served as SSE after setup succeeds. | Other HTTP methods return `METHOD_NOT_SUPPORTED`. ## Inputs For `GET`, input comes from the `input` query parameter: ```http GET /trpc/user.get?input={"id":"1"} ``` For `POST`, input comes from the raw request body: ```http POST /trpc/user.create Content-Type: application/json {"name":"Alice","email":"alice@example.com"} ``` Empty input is passed as the zero value for typed procedures or `nil` for void procedures. ## Success Envelope Normal query and mutation responses are wrapped in the tRPC result envelope: ```json { "result": { "data": { "id": "1", "name": "Alice" } } } ``` ## Error Envelope Errors use the tRPC error shape: ```json { "error": { "code": -32004, "message": "procedure not found", "data": { "code": "NOT_FOUND", "httpStatus": 404, "path": "user.missing" } } } ``` `WithDev(true)` adds `data.stack` for debugging. ## JSON Batching Batch requests use `?batch=1` and comma-separated procedure paths. For `GET`, the `input` query parameter is an object keyed by batch index: ```http GET /trpc/user.get,system.health?batch=1&input={"0":{"id":"1"}} ``` For `POST`, the body has the same indexed shape: ```json { "0": { "id": "1" }, "1": { "page": 1, "perPage": 20 } } ``` The response is an array of individual envelopes. If every item has the same HTTP status, the batch response uses that status. Mixed statuses return HTTP `207 Multi-Status`. Subscriptions cannot be batched. ## JSONL Batch Streaming Set `trpc-accept: application/jsonl` on a batch request to stream batch results as JSON lines. ```http GET /trpc/user.get,user.list?batch=1 trpc-accept: application/jsonl ``` JSONL batch calls execute concurrently. Chunks may arrive out of request order, and per-call errors are represented inside their chunks. The HTTP status is `200` after streaming starts. ## Handler Snapshot `trpc.NewHandler` builds a procedure map when the handler is created. Register or merge procedures before mounting the handler: ```go trpcgo.MustQuery(router, "user.get", getUser) handler := trpc.NewHandler(router, "/trpc") // This registration is not visible to handler. trpcgo.MustQuery(router, "user.late", lateHandler) ``` If you need dynamic routing, construct a new handler after updating registrations. --- # Subscriptions > Stream procedure results over Server-Sent Events with tracked IDs and reconnect support. Subscriptions expose a Go channel as a tRPC-compatible SSE stream. ## Register A Subscription ```go type RoomInput struct { RoomID string `json:"roomId" validate:"required"` } type Message struct { ID string `json:"id"` Text string `json:"text"` } trpcgo.MustSubscribe(router, "chat.messages", func(ctx context.Context, input RoomInput) (<-chan Message, error) { ch := make(chan Message) messages := broker.Subscribe(input.RoomID) go func() { defer close(ch) for { select { case <-ctx.Done(): return case msg, ok := <-messages: if !ok { return } select { case ch <- msg: case <-ctx.Done(): return } } } }() return ch, nil }) ``` The request setup phase can return a normal tRPC error. Once streaming starts, later errors are sent as SSE `serialized-error` events. ## Wire Events The server sends: | Event | Meaning | | --- | --- | | `connected` | First event. Includes `reconnectAfterInactivityMs` when configured. | | default `message` | Data emitted from the Go channel. | | `ping` | Keep-alive event. | | `return` | Stream completed or max duration reached. | | `serialized-error` | Stream failed after SSE started. | Data messages use the SSE default event type. They do not include an explicit `event: message` line. ## Client With EventSource ```ts const source = new EventSource('/trpc/user.onCreated'); source.onmessage = (event) => { const user = JSON.parse(event.data); console.log(user); }; source.addEventListener('serialized-error', (event) => { console.error(JSON.parse(event.data)); }); ``` ## Tracked Events Wrap values with `Tracked` to send an SSE `id` field: ```go ch <- trpcgo.Tracked("message-42", Message{ID: "42", Text: "hello"}) ``` The client receives only the wrapped data as JSON. The tracking ID is transport metadata, not part of the payload. Use `TrackedEvent` directly when you also need to set SSE retry: ```go ch <- trpcgo.TrackedEvent[Message]{ ID: "message-42", Retry: 5000, Data: Message{ID: "42", Text: "hello"}, } ``` trpcgo strips newlines from SSE IDs before writing them. ## Reconnect Input When the client reconnects with `Last-Event-Id`, trpcgo merges it into subscription input as `lastEventId`. ```go type StreamInput struct { LastEventID string `json:"lastEventId,omitempty"` } ``` Sources are checked in this order: - `Last-Event-Id` header. - `lastEventId` query parameter. - `Last-Event-Id` query parameter. Treat `lastEventId` as untrusted client input. ## Final Values Use `SubscribeWithFinal` when the stream should end with a final value: ```go trpcgo.MustSubscribeWithFinal(router, "job.progress", func(ctx context.Context, input JobInput) (<-chan Progress, func() any, error) { ch := make(chan Progress) final := func() any { return JobResult{Done: true} } return ch, final, nil }) ``` The final value is sent in the SSE `return` event. ## Limits Use router options to control stream behavior: ```go router := trpcgo.NewRouter( trpcgo.WithSSEPingInterval(5*time.Second), trpcgo.WithSSEMaxDuration(10*time.Minute), trpcgo.WithSSEMaxConnections(1000), trpcgo.WithSSEReconnectAfterInactivity(30*time.Second), ) ``` `WithSSEMaxConnections` rejects extra streams with `TOO_MANY_REQUESTS`. --- # Response Metadata > Set response headers and cookies from procedures, middleware, and server-side calls. trpcgo injects response metadata into the context before HTTP procedure execution. Handlers and middleware can add response headers or cookies without depending on `http.ResponseWriter`. ## Set Headers ```go func handler(ctx context.Context, input Input) (Output, error) { trpcgo.SetResponseHeader(ctx, "X-Trace-ID", traceIDFrom(ctx)) return Output{}, nil } ``` `SetResponseHeader` adds a header value. It is safe to call from JSONL batch handlers concurrently. ## Set Cookies ```go func login(ctx context.Context, input LoginInput) (User, error) { trpcgo.SetCookie(ctx, &http.Cookie{ Name: "session", Value: issueSession(input), Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, }) return user, nil } ``` Headers and cookies are applied before the status line is written. ## No-Op Outside Metadata Context If the context does not carry response metadata, `SetResponseHeader` and `SetCookie` are safe no-ops. The HTTP handler creates the metadata context automatically. ## RawCall `RawCall` injects response metadata if the context does not already have it, but callers only receive headers and cookies if they keep and inspect the context that carries metadata. ```go ctx := trpcgo.WithResponseMetadata(context.Background()) result, err := router.RawCall(ctx, "auth.login", rawInput) if err != nil { return err } headers := trpcgo.GetResponseHeaders(ctx) cookies := trpcgo.GetResponseCookies(ctx) _ = result _ = headers _ = cookies ``` Use this when server-side procedure calls need to observe cookies or custom headers set by handlers. --- # Errors > Return tRPC-compatible errors, customize formatting, and understand sanitization. trpcgo errors carry a tRPC code, a message, an optional cause, and an HTTP status mapping. ## Return Typed Errors ```go return User{}, trpcgo.NewError(trpcgo.CodeNotFound, "user not found") ``` ```go return nil, trpcgo.NewErrorf(trpcgo.CodeBadRequest, "invalid id: %s", id) ``` ```go return nil, trpcgo.WrapError(trpcgo.CodeInternalServerError, "database failed", err) ``` Common codes: | Go constant | tRPC name | HTTP status | | --- | --- | --- | | `CodeBadRequest` | `BAD_REQUEST` | `400` | | `CodeUnauthorized` | `UNAUTHORIZED` | `401` | | `CodeForbidden` | `FORBIDDEN` | `403` | | `CodeNotFound` | `NOT_FOUND` | `404` | | `CodeMethodNotSupported` | `METHOD_NOT_SUPPORTED` | `405` | | `CodePayloadTooLarge` | `PAYLOAD_TOO_LARGE` | `413` | | `CodeTooManyRequests` | `TOO_MANY_REQUESTS` | `429` | | `CodeInternalServerError` | `INTERNAL_SERVER_ERROR` | `500` | Other standard tRPC-compatible gateway, timeout, conflict, and precondition codes are also available. ## Sanitization Plain Go errors are converted to `INTERNAL_SERVER_ERROR` before they reach the client. Internal errors with causes are masked as `internal server error` in client responses. `WithOnError` runs before response formatting and can receive the wrapped cause for server-side logging. ```go router := trpcgo.NewRouter( trpcgo.WithOnError(func(ctx context.Context, err *trpcgo.Error, path string) { log.Printf("trpc error on %s: %v", path, err) }), ) ``` ## Dev Mode `WithDev(true)` adds Go stack traces to `error.data.stack`. It does not expose wrapped internal cause messages to clients. Keep dev mode off in production. ## Custom Error Formatter Use `WithErrorFormatter` to extend or replace the serialized error shape. ```go router := trpcgo.NewRouter( trpcgo.WithErrorFormatter(func(input trpcgo.ErrorFormatterInput) any { return map[string]any{ "error": map[string]any{ "code": input.Shape.Error.Code, "message": input.Shape.Error.Message, "data": input.Shape.Error.Data, "timestamp": time.Now().UTC().Format(time.RFC3339), }, } }), ) ``` `ErrorFormatterInput` includes the client-safe error, procedure type, path, raw JSON input, request context, and default tRPC error shape. > [!CAUTION] > The context may contain credentials or other sensitive values. Do not blindly serialize context values into error responses. ## Output Hook Errors Errors from output validators or output parsers become `INTERNAL_SERVER_ERROR`. For subscriptions, trpcgo sends an SSE `serialized-error` event and closes the stream. --- # Struct Tags > Control generated TypeScript fields, names, optionality, docs, and embedded struct behavior from Go tags. Go struct tags are the contract between your Go runtime and generated frontend code. They control how fields are named, whether they are optional, and how TypeScript output is customized. ## JSON Tags `json` tags control field names and optionality. ```go type User struct { ID string `json:"id"` Name string `json:"name"` Bio *string `json:"bio,omitempty"` } ``` Generated TypeScript: ```ts export interface User { id: string; name: string; bio?: string; } ``` Rules: - `json:"name"` sets the TypeScript property name. - `json:"-"` excludes the field. - `omitempty` and `omitzero` make the field optional. - No tag uses the Go field name. - Unexported fields are ignored. ## TypeScript Overrides Use `tstype` when Go's default type mapping needs help. ```go type User struct { ID string `json:"id" tstype:",readonly"` Preferences map[string]any `json:"prefs" tstype:"Record"` Internal string `json:"internal" tstype:"-"` Email *string `json:"email,omitempty" tstype:",required"` } ``` | Tag | Effect | | --- | --- | | `tstype:"SomeType"` | Replaces the generated TypeScript type. | | `tstype:",readonly"` | Emits a readonly property. | | `tstype:",required"` | Forces a pointer or `omitempty` field to be required. | | `tstype:"-"` | Excludes the field from generated TypeScript and Zod metadata. | | `tstype:",extends"` | For embedded structs, emits TypeScript `extends` instead of flattening. | Type overrides may include commas, such as `Record`. ## Field Documentation Static generation converts Go doc comments to JSDoc. ```go // User represents a registered user in the system. type User struct { // The unique identifier for this user. ID string `json:"id" tstype:",readonly"` } ``` Use `ts_doc` when you need documentation from a tag, including in runtime reflection generation: ```go type CreateUserInput struct { Name string `json:"name" ts_doc:"Human-readable display name."` } ``` Standard Zod output also uses `ts_doc` in `.describe(...)`. ## Embedded Structs Embedded structs are flattened by default. ```go type Base struct { ID string `json:"id"` } type User struct { Base Name string `json:"name"` } ``` Use `tstype:",extends"` to preserve inheritance in TypeScript: ```go type User struct { Base `tstype:",extends"` Name string `json:"name"` } ``` Pointer embedded extends become `Partial` unless marked `required`. ## Related Tags `validate` and `zod_omit` affect generated Zod schemas rather than the main TypeScript field contract. See [Zod Schemas](/zod-schemas/) for those rules. --- # Zod Schemas > Generate Zod input schemas from Go validate tags and use them on the frontend. trpcgo can generate Zod schemas for procedure input types. This lets frontend form validation share the same constraints described by your Go structs. ## Enable Zod Output With the static CLI: ```bash go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./... ``` With `go:generate`: ```go //go:generate go tool trpcgo generate -o ../web/gen/trpc.ts --zod ../web/gen/zod.ts ./... ``` With dev watch: ```go router := trpcgo.NewRouter( trpcgo.WithDev(true), trpcgo.WithTypeOutput("../web/gen/trpc.ts"), trpcgo.WithZodOutput("../web/gen/zod.ts"), ) ``` Zod generation targets typed procedure input types and their dependencies. It does not generate schemas for output-only types. ## Install Zod Generated schemas target Zod 4. ```bash npm install zod ``` ## Basic Example ```go type CreateUserInput struct { Name string `json:"name" validate:"required,min=1,max=100"` Email string `json:"email" validate:"required,email"` Role string `json:"role,omitempty" validate:"omitempty,oneof=admin editor viewer"` Bio *string `json:"bio,omitempty" validate:"omitempty,max=500"` } ``` Generated standard Zod resembles: ```ts export const CreateUserInputSchema = z .object({ name: z.string().min(1).max(100), email: z.email(), role: z.enum(['admin', 'editor', 'viewer']).optional().or(z.literal('')), bio: z.string().max(500).optional().or(z.literal('')), }) .meta({ id: 'CreateUserInput' }); ``` Use it in the frontend: ```ts import { CreateUserInputSchema } from '../gen/zod.js'; const parsed = CreateUserInputSchema.safeParse(formData); if (!parsed.success) { setErrors(parsed.error.flatten().fieldErrors); return; } await client.user.create.mutate(parsed.data); ``` ## Compose UI Schemas Generated schemas are useful as a server-contract base, but forms often need UI-specific rules. Compose them with normal Zod helpers instead of editing generated files: ```ts import { z } from 'zod'; import { CreateScheduleInputSchema } from '../gen/zod.js'; export const ScheduleFormSchema = CreateScheduleInputSchema.pick({ broadcaster_id: true, quality: true, has_min_viewers: true, min_viewers: true, }).extend({ broadcaster_id: z.string().min(1).regex(/^\d+$/), }).superRefine((value, ctx) => { if (value.has_min_viewers && value.min_viewers == null) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['min_viewers'], message: 'min_viewers is required when enabled', }); } }); ``` Use this pattern when form toggles, hidden fields, or route params make the browser-facing shape narrower than the tRPC input type. ## Runtime Validation Caveat `validate` tags do not run on the server unless you configure a validator. ```go validate := validator.New() router := trpcgo.NewRouter( trpcgo.WithValidator(validate.Struct), ) ``` Zod generation and server-side validation are related, but separate: - `--zod` or `WithZodOutput` generates frontend schemas. - `WithValidator(validate.Struct)` validates decoded inputs at runtime. ## Standard Zod Vs zod/mini Use `--zod-mini` or `WithZodMini(true)` to emit `zod/mini` functional syntax. ```bash go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts --zod-mini ./... ``` ```go router := trpcgo.NewRouter( trpcgo.WithZodOutput("../web/gen/zod.ts"), trpcgo.WithZodMini(true), ) ``` Standard Zod output includes `.meta({ id: "TypeName" })` and `.describe(...)` from `ts_doc`. `zod/mini` skips metadata features that mini does not support. ## Supported Validate Tags trpcgo supports common `go-playground/validator` tags including: | Category | Tags | | --- | --- | | Required/optional | `required`, `omitempty` | | Containers | `dive` | | Length/range | `min`, `max`, `len`, `gt`, `gte`, `lt`, `lte` | | Formats | `email`, `url`, `uuid`, `e164`, `jwt`, `base64`, `base64url`, `ip`, `ipv4`, `ipv6`, `hostname`, `ulid`, `mac`, `cidrv4`, `cidrv6` | | Strings | `alphanum`, `alpha`, `numeric`, `lowercase`, `uppercase`, `startswith`, `endswith`, `contains`, `hexadecimal` | | Enums | `oneof` | | Cross-field | `gtefield`, `ltefield`, `gtfield`, `ltfield`, `eqfield`, `nefield` | Unsupported tags are preserved as comments in generated schemas instead of silently disappearing. ## `omitempty` Semantics There are two separate concepts: - TypeScript optionality controls whether a field may be `undefined`. - Validator `omitempty` allows the Go zero value to pass constraints. For example, `validate:"omitempty,email"` allows an empty string and otherwise requires a valid email. Zod output reflects that with an `or(z.literal(''))` branch. ## Arrays And `dive` Rules before `dive` apply to the container. Rules after `dive` apply to elements. ```go type Input struct { Tags []string `json:"tags" validate:"min=1,dive,min=1,max=50"` } ``` Generated Zod applies `.min(1)` to the array and `.min(1).max(50)` to each string. ## Cross-Field Validation Cross-field tags generate object-level refinements using JSON field names. ```go type RangeInput struct { Start int `json:"start"` End int `json:"end" validate:"gtefield=Start"` } ``` The generated schema checks the relationship between `end` and `start` after individual fields parse. ## Zod-Only Omit Use `zod_omit:"true"` to keep a field in TypeScript but leave it out of generated Zod schemas. ```go type CreateUserInput struct { Name string `json:"name" validate:"required"` CSRFToken string `json:"csrfToken" zod_omit:"true"` } ``` This is useful when a field is supplied by transport or framework code rather than user form data. ## No Typed Inputs If no procedures have typed inputs, runtime `GenerateZod` and dev watch remove stale Zod files. The CLI can still create an empty file because it opens the requested output path before writing schemas. --- # Code Generation > Generate TypeScript AppRouter types and Zod schemas from Go procedures. Generation turns Go procedure registrations into the TypeScript contract consumed by tRPC clients. ## Recommended Production Flow Use static analysis before building the frontend: ```go //go:generate go tool trpcgo generate -o ../web/gen/trpc.ts --zod ../web/gen/zod.ts ./... ``` ```bash go generate ./... ``` Static analysis reads source packages with `go/packages`, so it can preserve source-only information that reflection cannot see. ## CLI ```bash go tool trpcgo generate [flags] [packages] ``` If no package pattern is provided, trpcgo analyzes `.`. | Flag | Description | | --- | --- | | `-o`, `-output` | TypeScript output file. Defaults to stdout. | | `-dir` | Working directory for package resolution. Defaults to `.`. | | `-w`, `-watch` | Watch Go files and regenerate on write/create events. | | `-zod` | Zod schema output file. | | `-zod-mini` | Emit `zod/mini` functional syntax. | Examples: ```bash go tool trpcgo generate -o web/gen/trpc.ts ./... go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./... go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts -w ./... ``` > [!CAUTION] > The CLI writes `-o` and `-zod` paths directly. Create parent directories before running the command. ## Runtime And Dev Generation The router can write generated files from registered procedure reflection types: ```go if err := router.GenerateTS("web/gen/trpc.ts"); err != nil { return err } if err := router.GenerateZod("web/gen/zod.ts"); err != nil { return err } ``` In normal development, prefer the integrated watcher: ```go router := trpcgo.NewRouter( trpcgo.WithDev(true), trpcgo.WithTypeOutput("../web/gen/trpc.ts"), trpcgo.WithZodOutput("../web/gen/zod.ts"), ) defer router.Close() ``` When `trpc.NewHandler` is constructed, trpcgo starts the watcher. It generates once from source, then regenerates when `.go` files change. If source analysis fails because the Go code is temporarily broken, previous generated files are preserved. Use `WithWatchPackages` to avoid watching unrelated frontend or generated directories in larger repositories: ```go trpcgo.WithWatchPackages("./cmd/api", "./internal/...") ``` ## Static Analysis Vs Reflection | Feature | Static CLI | Runtime reflection | | --- | --- | --- | | Registered procedure input/output types | Yes | Yes | | `json`, `tstype`, `validate`, `ts_doc`, `zod_omit` tags | Yes | Yes | | Go doc comments as JSDoc | Yes | No | | const groups as string/number unions | Yes | No | | aliases and defined basic types | Yes | Limited | | source-level typed output parser discovery | Yes | Registered typed parsers only | Use the CLI for generated files committed or built in CI. Use dev watch for a fast local feedback loop. ## Generated TypeScript Shape Generated `trpc.ts` includes: - `// Code generated by trpcgo. DO NOT EDIT.` - Type-only imports from `@trpc/server`. - Exported TypeScript definitions for reachable Go types. - `$Query`, `$Mutation`, and `$Subscription` helper aliases only when needed. - Nested `AppRouterRecord` generated from dot-separated procedure paths. - Exported `AppRouter`. - `RouterInputs` and `RouterOutputs` helpers when procedures exist. Procedure paths become nested objects: ```go trpcgo.MustQuery(router, "user.get", getUser) trpcgo.MustMutation(router, "admin.user.ban", banUser) ``` ```ts type AppRouterRecord = { user: { get: $Query; }; admin: { user: { ban: $Mutation; }; }; }; ``` ## Type Mapping Common Go-to-TypeScript mappings: | Go | TypeScript | | --- | --- | | `string` | `string` | | `bool` | `boolean` | | numeric types | `number` | | `time.Time` | `string` | | `[]T`, `[N]T` | `T[]` | | `[]byte` | `string` | | `map[K]V` | `Record` | | `any`, `interface{}` | `unknown` | | `json.RawMessage` | `unknown` | | `json.Number` | `number` | | `TrackedEvent[T]` | `T` | Pointer fields and fields tagged `omitempty` or `omitzero` become optional unless overridden with `tstype:",required"`. ## Detection Rules And Limits Static generation discovers calls to the top-level registration functions such as `Query`, `Mutation`, `Subscribe`, `SubscribeWithFinal`, and their `Void`/`Must` variants. Important limits: - Procedure paths must be string literals for static analysis. - Packages must load and type-check successfully. - Custom wrapper functions are only detected if the analyzer can see the underlying top-level registration call with a literal path. - Zod generation targets procedure input types and their dependencies, not output-only types. - Reflection generation cannot emit source comments or const unions. ## Zod Output Pass `--zod` or configure `WithZodOutput` to generate schemas for typed procedure inputs. ```bash go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./... ``` If no procedures have typed inputs, runtime `GenerateZod` and dev watch remove stale Zod files. The CLI can still create an empty file. See [Zod Schemas](/zod-schemas/) for validate tag mapping, `zod/mini`, `omitempty`, `dive`, cross-field rules, and frontend usage. --- # Frontend Setup > Use generated trpcgo router types with tRPC clients, React Query, TanStack Router, and generated Zod schemas. The generated `AppRouter` type plugs into normal tRPC v11 clients. ## Vanilla Client ```ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../gen/trpc.js'; export const client = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:8080/trpc', }), ], }); const user = await client.user.get.query({ id: '1' }); const created = await client.user.create.mutate({ name: 'Alice', email: 'alice@example.com' }); ``` ## React Query ```ts import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../gen/trpc.js'; export const trpc = createTRPCReact(); ``` Create a client with query batching and SSE subscription routing: ```ts import { httpBatchLink, httpSubscriptionLink, splitLink } from '@trpc/client'; const trpcClient = trpc.createClient({ links: [ splitLink({ condition: (op) => op.type === 'subscription', true: httpSubscriptionLink({ url: '/trpc' }), false: httpBatchLink({ url: '/trpc' }), }), ], }); ``` For cookie-authenticated apps served from a different origin in development, configure credentials on both transports: ```ts import { createTRPCClient, httpBatchLink, httpSubscriptionLink, splitLink } from '@trpc/client'; import type { AppRouter } from '../gen/trpc.js'; const trpcClient = createTRPCClient({ links: [ splitLink({ condition: (op) => op.type === 'subscription', true: httpSubscriptionLink({ url: `${API_URL}/trpc`, eventSourceOptions: { withCredentials: true }, }), false: httpBatchLink({ url: `${API_URL}/trpc`, fetch(url, options) { return fetch(url, { ...options, credentials: 'include' }); }, }), }), ], }); ``` ## TanStack React Query Helpers With `@trpc/tanstack-react-query`, create a typed context and provider: ```tsx import { QueryClient } from '@tanstack/react-query'; import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { ReactNode } from 'react'; import type { AppRouter } from '../gen/trpc.js'; export const { TRPCProvider, useTRPC } = createTRPCContext(); const queryClient = new QueryClient(); const trpcClient = createTRPCClient({ links: [httpBatchLink({ url: '/trpc' })], }); export function AppProviders({ children }: { children: ReactNode }) { return ( {children} ); } ``` Then use generated query and mutation options: ```ts const users = useQuery(trpc.user.list.queryOptions({ page: 1, perPage: 20 })); const createUser = useMutation({ ...trpc.user.create.mutationOptions(), onSuccess: () => queryClient.invalidateQueries({ queryKey: [['user']] }), }); ``` ## RouterInputs And RouterOutputs Generated helpers let you reuse exact procedure types in UI code. ```ts import type { RouterInputs, RouterOutputs } from '../gen/trpc.js'; type CreateUserInput = RouterInputs['user']['create']; type CreatedUser = RouterOutputs['user']['create']; ``` ## Client-Side Zod Validation Generated schemas match Go `validate` tags for procedure inputs. ```ts import { CreateUserInputSchema } from '../gen/zod.js'; const parsed = CreateUserInputSchema.safeParse(formData); if (!parsed.success) { setErrors(parsed.error.flatten().fieldErrors); return; } await client.user.create.mutate(parsed.data); ``` See [Zod Schemas](/zod-schemas/) for generation options and supported validation tags. ## Subscriptions With EventSource Subscriptions are SSE streams. You can consume them directly: ```ts const source = new EventSource('/trpc/user.onCreated'); source.onmessage = (event) => { const user = JSON.parse(event.data); console.log(user); }; source.addEventListener('serialized-error', (event) => { console.error(JSON.parse(event.data)); }); ``` If you use tRPC's subscription link, route subscription operations to `httpSubscriptionLink` and queries/mutations to `httpBatchLink`. --- # Security & Production > Configure validation, input strictness, request limits, CORS, errors, and subscriptions for production. trpcgo provides protocol-level safety checks, but application security still belongs in your middleware, validators, and deployment configuration. ## Enable Runtime Validation `validate` tags generate Zod schemas, but they do not run on the server unless you configure a validator. ```go validate := validator.New() router := trpcgo.NewRouter( trpcgo.WithValidator(validate.Struct), ) ``` Validation runs after JSON decoding and only for struct inputs. ## Reject Unknown Fields Go's default JSON decoder ignores unknown fields. Enable strict input when you want unknown fields rejected: ```go router := trpcgo.NewRouter( trpcgo.WithStrictInput(true), ) ``` Strict input also applies to `RawCall`. ## Keep Request Limits Defaults are conservative: | Limit | Default | | --- | --- | | Max body/query input size | `1 MiB` | | Max batch size | `10` procedures | | SSE max duration | `30m` | | SSE max connections | unlimited | Tune them explicitly for public APIs: ```go router := trpcgo.NewRouter( trpcgo.WithMaxBodySize(512<<10), trpcgo.WithMaxBatchSize(20), trpcgo.WithSSEMaxConnections(1000), trpcgo.WithSSEMaxDuration(10*time.Minute), ) ``` Use `-1` only when you intentionally want an unlimited setting. ## Keep Dev Mode Off `WithDev(true)` adds stack traces to error responses and enables dev generation behavior. Use it locally, not in production. ```go router := trpcgo.NewRouter( trpcgo.WithDev(os.Getenv("APP_ENV") != "production"), ) ``` Internal error messages are still masked, but stack traces can reveal implementation details. ## Sanitize Error Formatting Custom error formatters receive request context and raw JSON input. Do not echo secrets, auth tokens, cookies, or arbitrary context values into client responses. Good formatter pattern: ```go trpcgo.WithErrorFormatter(func(input trpcgo.ErrorFormatterInput) any { return map[string]any{ "error": map[string]any{ "code": input.Shape.Error.Code, "message": input.Shape.Error.Message, "data": input.Shape.Error.Data, }, } }) ``` Use `WithOnError` for detailed server-side logs instead. ## Add CORS Yourself trpcgo does not handle CORS. Add it in your HTTP middleware if browsers call the API from another origin. ```go handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "https://app.example.com") w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID, trpc-accept") w.Header().Add("Vary", "Origin") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } trpcHandler.ServeHTTP(w, r) }) ``` Only set `Access-Control-Allow-Credentials: true` when you intentionally use cookies or other credentialed browser requests, and do not combine it with `Access-Control-Allow-Origin: *`. ## Protect Cookie-Authenticated Browsers CORS controls which browsers can read responses. It does not by itself protect cookie-authenticated mutation requests from cross-site form or fetch attempts. For browser dashboards that authenticate with cookies, mount the tRPC handler behind origin/CSRF protection and explicitly trust only your frontend origins: ```go trpcHandler := trpc.NewHandler(router, "/trpc") csrf := http.NewCrossOriginProtection() if err := csrf.AddTrustedOrigin("https://app.example.com"); err != nil { log.Fatal(err) } mux.Handle("/trpc/", csrf.Handler(trpcHandler)) ``` Pair this with credentialed frontend links when the dashboard and API use different origins in development. ## Treat Reconnect IDs As Untrusted For subscriptions, `Last-Event-Id` is merged into input as `lastEventId`. Validate it like any other client input before using it as a cursor. ```go type StreamInput struct { LastEventID string `json:"lastEventId,omitempty" validate:"omitempty,max=200"` } ``` ## Register Before Serving Because `trpc.NewHandler` snapshots procedures, construct the handler after all routes and middleware are registered. Creating the handler too early can accidentally leave procedures unserved. ## Generate In CI For production builds, run static generation before building the frontend: ```bash go generate ./... npm run build ``` Do not rely on dev watch as a production build step. --- # CLI > Reference for the trpcgo generate command. ```bash go tool trpcgo generate [flags] [packages] ``` If no package patterns are supplied, the CLI analyzes `.`. ## Flags | Flag | Description | | --- | --- | | `-o`, `-output` | Write generated TypeScript router types to a file. Defaults to stdout. | | `-dir` | Working directory for Go package resolution. Defaults to `.`. | | `-w`, `-watch` | Watch Go files and regenerate on changes. | | `-zod` | Write generated Zod 4 schemas to a file. | | `-zod-mini` | Generate schemas using `zod/mini` functional syntax. | ## Examples Generate TypeScript router types: ```bash go tool trpcgo generate -o web/gen/trpc.ts ./... ``` Generate TypeScript router types and Zod schemas: ```bash go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./... ``` Generate from another working directory: ```bash go tool trpcgo generate -dir ./server -o ../web/gen/trpc.ts --zod ../web/gen/zod.ts ./... ``` Watch during development: ```bash go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts -w ./... ``` ## Detection Rules The static analyzer detects calls to trpcgo registration functions: - `Query`, `VoidQuery`, `Mutation`, `VoidMutation`, `Subscribe`, `VoidSubscribe`. - `SubscribeWithFinal`, `VoidSubscribeWithFinal`. - All `Must*` variants. Procedure paths must be string literals. ```go trpcgo.MustQuery(router, "user.get", getUser) // detected path := "user.get" trpcgo.MustQuery(router, path, getUser) // not detected by static generation ``` ## Output Paths The CLI creates output files with `os.Create`. Create parent directories first: ```bash mkdir -p web/gen go tool trpcgo generate -o web/gen/trpc.ts --zod web/gen/zod.ts ./... ``` Runtime dev generation creates missing parent directories automatically. ## Watch Mode Watch mode runs generation once, then watches Go files under `-dir` recursively. It ignores common heavy directories such as `.git`, `vendor`, `node_modules`, `testdata`, `dist`, `build`, and `coverage`. Only `.go` file create/write events trigger regeneration. --- # Compatibility > Supported Go, tRPC client, HTTP, Zod, and CORS expectations. ## Go trpcgo requires Go 1.26 or newer. The project uses modern Go features including generics, tool directives, and `errors.AsType`. ## tRPC Client Generated router types target tRPC v11 client packages. The generated `AppRouter` type imports from `@trpc/server`, which should be installed in the frontend package alongside `@trpc/client`. ## Zod Generated schemas target Zod 4. Use `--zod-mini` or `WithZodMini(true)` to generate `zod/mini` functional syntax instead of standard chained syntax. ## HTTP The runtime is plain `net/http`. No web framework is required. You can mount `trpc.NewHandler(router, basePath)` behind any router or middleware stack that can serve an `http.Handler`. ## CORS trpcgo does not implement CORS. Add CORS in your web framework, HTTP middleware, reverse proxy, or edge layer. ## Serialization trpcgo uses JSON over the tRPC HTTP protocol. Notable mappings: - `time.Time` is represented as a string in TypeScript. - `[]byte` is represented as a string. - `json.RawMessage`, `any`, and `interface{}` become `unknown`. - `int64` and `uint64` generate number-based Zod schemas because JSON sends numbers, not JavaScript `bigint` values. ## Example App `examples/start-trpc/` contains a complete Go server and TanStack Start frontend showing: - Queries, mutations, void procedures, and SSE subscriptions. - Generated `AppRouter`, `RouterInputs`, and `RouterOutputs`. - Generated Zod schemas used for form validation. - Middleware, metadata, custom error formatting, and server-side `Call`.