trpcgo

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Mar 2, 2026 License: MIT Imports: 21 Imported by: 0

README

trpcgo

Write Go structs and handlers, get TypeScript types automatically.

Warning: This project is under active development. APIs may change and things may break.

Go structs + handlers  →  trpcgo generate  →  TypeScript AppRouter + Zod schemas

trpcgo is a Go runtime library and code generator that gives you the tRPC developer experience: change a Go struct, save the file, and your TypeScript types update instantly. No manual type syncing, no OpenAPI specs, no protobuf.

Install

# Add to your Go module
go get github.com/befabri/trpcgo@latest

# Install the CLI (Go 1.26+ tool directive)
# In your go.mod:
tool github.com/befabri/trpcgo/cmd/trpcgo

Quick Start

1. Define types and handlers in Go
//go:generate go tool trpcgo generate -o ../web/gen/trpc.ts --zod ../web/gen/zod.ts

package main

import (
    "context"
    "github.com/befabri/trpcgo"
)

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) {
    // your logic here
    return User{ID: "1", Name: input.Name, Email: input.Email}, nil
}

func main() {
    router := trpcgo.NewRouter(
        trpcgo.WithTypeOutput("../web/gen/trpc.ts"),
        trpcgo.WithZodOutput("../web/gen/zod.ts"),
    )
    trpcgo.Mutation(router, "user.create", CreateUser)

    http.ListenAndServe(":8080", router.Handler("/trpc"))
}
2. Generated TypeScript (automatic)

trpc.ts — full AppRouter type:

export interface CreateUserInput {
  name: string;
  email: string;
}

export interface User {
  readonly id: string;
  name: string;
  email: string;
}

export type AppRouter = { /* ... structural types matching @trpc/client */ };

zod.ts — validation schemas from Go validate tags:

import { z } from "zod";

export const CreateUserInputSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.email(),
});
3. Use in your frontend
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../gen/trpc.js";

export const trpc = createTRPCReact<AppRouter>();

// Fully typed — input and output inferred from Go types
const mutation = trpc.user.create.useMutation();
mutation.mutate({ name: "Alice", email: "alice@example.com" });

Procedure Types

// Query — read operation with typed input
trpcgo.Query(router, "user.getById", func(ctx context.Context, input GetUserInput) (User, error) {
    return db.FindUser(input.ID)
})

// VoidQuery — read operation with no input
trpcgo.VoidQuery(router, "system.health", func(ctx context.Context) (HealthInfo, error) {
    return HealthInfo{OK: true}, nil
})

// Mutation — write operation with typed input
trpcgo.Mutation(router, "user.create", func(ctx context.Context, input CreateUserInput) (User, error) {
    return db.CreateUser(input)
})

// VoidMutation — write operation with no input
trpcgo.VoidMutation(router, "system.reset", func(ctx context.Context) (string, error) {
    return "done", nil
})

// Subscribe — SSE subscription with typed input
trpcgo.Subscribe(router, "chat.messages", func(ctx context.Context, input RoomInput) (<-chan Message, error) {
    ch := make(chan Message)
    // push messages to ch, close when ctx.Done()
    return ch, nil
})

// VoidSubscribe — SSE subscription with no input
trpcgo.VoidSubscribe(router, "user.onCreated", func(ctx context.Context) (<-chan User, error) {
    ch := make(chan User)
    // push to ch when users are created
    return ch, nil
})

Router Options

router := trpcgo.NewRouter(
    // Request handling
    trpcgo.WithBatching(true),               // enable batch requests
    trpcgo.WithMethodOverride(true),          // allow POST for queries
    trpcgo.WithMaxBodySize(2 << 20),          // 2MB request limit (default 1MB)

    // Validation
    trpcgo.WithValidator(validate.Struct),     // go-playground/validator compatible

    // SSE subscriptions
    trpcgo.WithSSEPingInterval(5 * time.Second),
    trpcgo.WithSSEMaxDuration(10 * time.Minute),
    trpcgo.WithSSEReconnectAfterInactivity(30 * time.Second),

    // Errors
    trpcgo.WithDev(true),                     // stack traces in error responses
    trpcgo.WithOnError(func(ctx context.Context, err *trpcgo.Error, path string) {
        log.Printf("error on %s: %v", path, err)
    }),
    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,
            },
        }
    }),

    // Context
    trpcgo.WithContextCreator(func(r *http.Request) context.Context {
        return context.WithValue(r.Context(), authKey, r.Header.Get("Authorization"))
    }),

    // Code generation (auto-regenerates on file save)
    trpcgo.WithTypeOutput("../web/gen/trpc.ts"),
    trpcgo.WithZodOutput("../web/gen/zod.ts"),
    trpcgo.WithZodMini(false),                // true for zod/mini syntax
)

Middleware

Global middleware
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
trpcgo.Mutation(router, "user.create", handler,
    trpcgo.Use(authRequired, rateLimiter),
    trpcgo.WithMeta(map[string]string{"action": "write"}),
)
Accessing metadata in middleware
func authRequired(next trpcgo.HandlerFunc) trpcgo.HandlerFunc {
    return func(ctx context.Context, input any) (any, error) {
        meta, _ := trpcgo.GetProcedureMeta(ctx)
        // meta.Path = "user.create"
        // meta.Type = "mutation"
        // meta.Meta = map[string]string{"action": "write"}
        return next(ctx, input)
    }
}

Errors

// Create errors with tRPC error codes
trpcgo.NewError(trpcgo.CodeNotFound, "user not found")
trpcgo.NewErrorf(trpcgo.CodeBadRequest, "invalid id: %s", id)
trpcgo.WrapError(trpcgo.CodeInternalServerError, "db failed", err)

Error codes follow the tRPC/JSON-RPC convention:

Code Name HTTP Status
-32700 CodeParseError 400
-32600 CodeBadRequest 400
-32001 CodeUnauthorized 401
-32003 CodeForbidden 403
-32004 CodeNotFound 404
-32603 CodeInternalServerError 500
-32029 CodeTooManyRequests 429
-32008 CodeTimeout 408

Server-Side Caller

Call procedures from within your Go code, running the full middleware chain:

// Typed call — input/output marshaled automatically
user, err := trpcgo.Call[CreateUserInput, User](router, ctx, "user.create", input)

// Raw call — JSON in, any out
result, err := router.RawCall(ctx, path, jsonBytes)

Struct Tags

JSON mapping

Standard json tags control field names and optionality:

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Bio  string `json:"bio,omitempty"` // optional in TypeScript
}
TypeScript overrides

The tstype tag controls TypeScript generation:

type User struct {
    ID          string         `json:"id" tstype:",readonly"`          // readonly id: string
    Preferences map[string]any `json:"prefs" tstype:"Record<string, unknown>"`
    Internal    string         `json:"internal" tstype:"-"`            // excluded from TS
    Email       string         `json:"email" tstype:",required"`       // never optional
}
Validation

validate tags (go-playground/validator) generate both server-side validation and Zod schemas:

type Input struct {
    Name  string   `json:"name" validate:"required,min=1,max=100"`    // z.string().min(1).max(100)
    Email string   `json:"email" validate:"required,email"`           // z.email()
    Role  string   `json:"role" validate:"oneof=admin editor viewer"` // z.enum([...])
    Tags  []string `json:"tags" validate:"min=1,dive,min=1,max=50"`   // z.array(z.string().min(1).max(50)).min(1)
    Age   int      `json:"age" validate:"gte=18,lte=150"`             // z.int().gte(18).lte(150)
    URL   string   `json:"url" validate:"url"`                        // z.url()
    UUID  string   `json:"uuid" validate:"uuid"`                      // z.uuidv4()
}

CLI

trpcgo generate [flags] [packages]
Flag Description
-o, --output TypeScript output file (default: stdout)
-dir Working directory (default: .)
-w, --watch Watch Go files, regenerate on change
--zod Zod schema output file
--zod-mini Use zod/mini functional syntax
With go:generate
//go:generate go tool trpcgo generate -o ../web/gen/trpc.ts --zod ../web/gen/zod.ts
go generate ./...
Watch mode
go tool trpcgo generate -o ../web/gen/trpc.ts --zod ../web/gen/zod.ts -w
Runtime watch (zero config)

When you set WithTypeOutput (and optionally WithZodOutput) on the router, Handler() starts a file watcher automatically. Save a .go file anywhere in the project tree and types regenerate instantly — no separate process needed.

Frontend Setup

// trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../gen/trpc.js";

export const trpc = createTRPCReact<AppRouter>();
// main.tsx
import { httpBatchLink, splitLink, unstable_httpSubscriptionLink } from "@trpc/client";

const trpcClient = trpc.createClient({
  links: [
    splitLink({
      condition: (op) => op.type === "subscription",
      true: unstable_httpSubscriptionLink({ url: "/trpc" }),
      false: httpBatchLink({ url: "/trpc" }),
    }),
  ],
});
Vanilla client
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "../gen/trpc.js";

const client = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: "http://localhost:8080/trpc" })],
});

const user = await client.user.getById.query({ id: "1" });

Router Merging

Split procedures across files and merge:

userRouter := trpcgo.NewRouter()
trpcgo.Query(userRouter, "user.list", listUsers)

adminRouter := trpcgo.NewRouter()
trpcgo.Mutation(adminRouter, "admin.ban", banUser)

router := trpcgo.NewRouter()
router.Merge(userRouter, adminRouter)
// or: router := trpcgo.MergeRouters(userRouter, adminRouter)

Example

See examples/tanstack-query/ for a full working example with:

  • Go server with all procedure types (Query, VoidQuery, Mutation, VoidMutation, VoidSubscribe)
  • Global and per-procedure middleware
  • Input validation with go-playground/validator
  • Custom error formatter
  • Server-side caller (trpcgo.Call)
  • SSE subscriptions with live broadcasting
  • React frontend with TanStack Router + React Query
  • Pagination, Zod validation, real-time feed
# Start the Go server
cd examples/tanstack-query/go-server
go run .

# In another terminal, start the frontend
cd examples/tanstack-query/web
npm install
npm run dev

How It Works

trpcgo has two code generation paths:

  1. Static analysis (trpcgo generate) — reads Go source via go/packages, extracts types with full fidelity (comments, validate tags, const unions). This is what generates Zod schemas.

  2. Runtime reflection (Router.GenerateTS) — uses reflect to inspect registered procedure types at startup. Faster but less information (no comments, no validate tags).

When you use WithTypeOutput, both paths run: reflection generates types immediately on startup, then a file watcher runs static analysis in the background and overwrites with the richer version. On subsequent file saves, only static analysis runs.

The file watcher is recursive — it watches all subdirectories and handles directory creation/removal automatically. Generated files are only written when content changes, avoiding spurious Vite HMR cycles.

License

MIT

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:

Void variants are for procedures that take no input.

Router Options

Configure the router with functional options:

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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Call

func Call[I any, O any](r *Router, ctx context.Context, path string, input I) (O, error)

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

func GetResponseCookies(ctx context.Context) []*http.Cookie

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

func GetResponseHeaders(ctx context.Context) http.Header

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

func HTTPStatusFromCode(code ErrorCode) int

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

func NameFromCode(code ErrorCode) string

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

func SetCookie(ctx context.Context, c *http.Cookie)

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

func SetResponseHeader(ctx context.Context, key, value string)

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

func WithResponseMetadata(ctx context.Context) context.Context

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

type Error struct {
	Code    ErrorCode
	Message string
	Cause   error
}

Error represents a tRPC error with a JSON-RPC 2.0 error code.

func NewError

func NewError(code ErrorCode, message string) *Error

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 NewErrorf

func NewErrorf(code ErrorCode, format string, args ...any) *Error

NewErrorf creates a new tRPC error with a formatted message.

func WrapError

func WrapError(code ErrorCode, message string, cause error) *Error

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

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

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
	CodeUnauthorized         ErrorCode = -32001
	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
	CodeServiceUnavailable   ErrorCode = -32503
	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

type HandlerFunc func(ctx context.Context, input any) (any, error)

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

func WithBatching(enabled bool) Option

WithBatching enables or disables batch request support.

func WithContextCreator

func WithContextCreator(fn func(r *http.Request) context.Context) Option

WithContextCreator sets a function that creates the base context for each request.

func WithDev

func WithDev(enabled bool) Option

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

func WithMaxBatchSize(n int) Option

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

func WithMaxBodySize(n int64) Option

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

func WithMethodOverride(enabled bool) Option

WithMethodOverride allows clients to override HTTP method (send queries as POST).

func WithOnError

func WithOnError(fn func(ctx context.Context, err *Error, path string)) Option

WithOnError sets a callback invoked when a procedure returns an error.

func WithSSEMaxDuration

func WithSSEMaxDuration(d time.Duration) Option

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

func WithSSEPingInterval(d time.Duration) Option

WithSSEPingInterval sets the keep-alive ping interval for SSE subscriptions. Default is 10 seconds.

func WithSSEReconnectAfterInactivity

func WithSSEReconnectAfterInactivity(d time.Duration) Option

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

func WithStrictInput(enabled bool) Option

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

func WithTypeOutput(path string) Option

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

func WithValidator(fn func(any) error) Option

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

func WithZodMini(enabled bool) Option

WithZodMini switches Zod schema output to zod/mini functional syntax. Only has effect when WithZodOutput is also set.

func WithZodOutput

func WithZodOutput(path string) Option

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 Use

func Use(mw ...Middleware) ProcedureOption

Use adds per-procedure middleware.

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

func MergeRouters(routers ...*Router) *Router

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

func NewRouter(opts ...Option) *Router

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

func (r *Router) GenerateTS(outputPath string) error

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

func (r *Router) GenerateZod(outputPath string) error

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

func (r *Router) Handler(basePath string) http.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

func (r *Router) Merge(sources ...*Router)

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

func (r *Router) RawCall(ctx context.Context, path string, input json.RawMessage) (any, error)

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

type TrackedEvent[T any] struct {
	ID   string
	Data T
}

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.

Directories

Path Synopsis
cmd
trpcgo command
internal
fsutil
Package fsutil provides filesystem utilities for the trpcgo watcher.
Package fsutil provides filesystem utilities for the trpcgo watcher.

Jump to

Keyboard shortcuts

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