trpcgo

package module
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Mar 17, 2026 License: MIT Imports: 23 Imported by: 0

README

trpcgo

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

trpcgo is a Go implementation of the tRPC protocol. You get the same end-to-end type safety as a TypeScript backend, but your server is written in Go. Define your API with Go structs and handlers, and trpcgo generates the TypeScript AppRouter type that plugs directly into @trpc/client and @trpc/react-query. No manual type syncing, no OpenAPI specs, no protobuf.

Table of Contents

Why

tRPC gives you end-to-end typesafe APIs: change a type on the server and TypeScript catches every broken call site at compile time. But tRPC requires a TypeScript server.

trpcgo removes that constraint. Write your server in Go and still get the full tRPC developer experience on the frontend. Your TypeScript client code looks exactly the same as if the server were written in TypeScript.

Install

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

# Install the code generator (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"
    "net/http"

    "github.com/befabri/trpcgo"
    "github.com/befabri/trpcgo/trpc"
)

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 main() {
    router := trpcgo.NewRouter(
        trpcgo.WithDev(true),
        trpcgo.WithTypeOutput("../web/gen/trpc.ts"),
        trpcgo.WithZodOutput("../web/gen/zod.ts"),
    )
    defer router.Close()

    trpcgo.MustMutation(router, "user.create", func(ctx context.Context, input CreateUserInput) (User, error) {
        return User{ID: "1", Name: input.Name, Email: input.Email}, nil
    })

    mux := http.NewServeMux()
    mux.Handle("/trpc/", trpc.NewHandler(router, "/trpc"))
    http.ListenAndServe(":8080", mux)
}
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 with @trpc/client
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

trpcgo supports all tRPC procedure types: queries, mutations, and subscriptions.

Each registration function returns an error (duplicate path). The Must* variants panic instead and are the idiomatic choice for application bootstrap code:

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

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

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

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

// Subscribe (SSE, with input)
trpcgo.MustSubscribe(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, no input)
trpcgo.MustVoidSubscribe(router, "user.onCreated", func(ctx context.Context) (<-chan User, error) {
    ch := make(chan User)
    // push to ch when users are created
    return ch, nil
})

// Non-Must variants return error — use when you need to handle the failure:
if err := trpcgo.Query(router, "user.getById", handler); err != nil {
    log.Fatal(err)
}

Base Procedures

trpcgo.Procedure() creates a reusable builder that bundles middleware and metadata — the Go equivalent of tRPC's composable procedure pattern. Builders are immutable: every chain call returns a new instance, so sharing a base never causes accidental mutation.

// Define reusable base procedures once
publicProcedure := trpcgo.Procedure()
authedProcedure := publicProcedure.Use(authMiddleware)
adminProcedure  := authedProcedure.Use(adminCheckMiddleware).WithMeta(roleMeta{Admin: true})

// Use them at every registration site
trpcgo.MustQuery(router,    "user.list",    listUsers,  authedProcedure)
trpcgo.MustMutation(router, "user.create",  createUser, authedProcedure)
trpcgo.MustMutation(router, "admin.ban",    banUser,    adminProcedure)

// Combine with per-procedure options — all options merge
trpcgo.MustQuery(router, "report.get", getReport, authedProcedure, trpcgo.WithMeta(auditLog{}))

Builders can also be seeded from an existing builder:

// Inherits all of authedProcedure's middleware, then adds more
orgProcedure := trpcgo.Procedure(authedProcedure).Use(orgScopeMiddleware)

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),     // default 30m, -1 for unlimited
    trpcgo.WithSSEMaxConnections(1000),               // concurrent SSE limit
    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 in dev mode)
    trpcgo.WithTypeOutput("../web/gen/trpc.ts"),
    trpcgo.WithZodOutput("../web/gen/zod.ts"),
    trpcgo.WithZodMini(false),                // true for zod/mini syntax
    trpcgo.WithWatchPackages("./internal/...", "./cmd/api"), // scope watcher to specific packages
)

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.MustMutation(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)

All standard tRPC error codes are available (CodeNotFound, CodeUnauthorized, CodeTooManyRequests, etc.) and map to the correct HTTP status codes.

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
}
Output Validation And Parsing

Use output hooks when a procedure should validate or transform its handler result before it is sent.

  • OutputValidator[O] validates the handler output without changing its type.
  • WithOutputValidator(func(any) error) is the builder-friendly untyped validator form.
  • OutputParser[O, P] is the typed form and updates generated output types to P.
  • WithOutputParser(func(any) (any, error)) is the builder-friendly untyped form; codegen falls back to unknown unless a typed OutputParser override is present.
// Typed: validate only
trpcgo.MustQuery(router, "user.get", getUser,
    trpcgo.OutputValidator(func(u User) error {
        if u.ID == "" { return errors.New("id required") }
        return nil
    }),
)

// Typed: validate or transform the output
trpcgo.MustQuery(router, "user.get", getUser,
    trpcgo.OutputParser(func(u User) (User, error) {
        if u.ID == "" { return User{}, errors.New("id required") }
        return u, nil
    }),
)

// Typed — transform (strip sensitive fields before sending to client)
type PublicUser struct { ID string `json:"id"` }
trpcgo.MustQuery(router, "user.get", getUser,
    trpcgo.OutputParser(func(u User) (PublicUser, error) {
        return PublicUser{ID: u.ID}, nil
    }),
)

// Untyped: useful on reusable builders
authedProcedure := trpcgo.Procedure().Use(authMW).
    WithOutputValidator(func(v any) error {
        return nil
    }).
    WithOutputParser(func(v any) (any, error) {
        // validate or transform v
        return v, nil
    })

Parser failures return INTERNAL_SERVER_ERROR. Clients and WithErrorFormatter(...) see a generic internal server error, while WithOnError(...) still receives the original wrapped cause for logging.

When both are present, the output validator runs before the output parser. For subscriptions, both run on each emitted item before TrackedEvent unwrapping. If either fails, the server sends a serialized-error SSE event and closes the stream.

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 WithDev(true) with WithTypeOutput (and optionally WithZodOutput) on the router, trpc.NewHandler starts a file watcher automatically. Save a .go file anywhere in the project tree and types regenerate instantly, no separate process needed. Call router.Close() to stop the watcher on shutdown.

Use WithWatchPackages to restrict watching to specific packages (go/packages patterns) — useful in monorepos to avoid watching unrelated directories like frontend build output.

Frontend Setup

React Query
// 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.MustQuery(userRouter, "user.list", listUsers)

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

router := trpcgo.NewRouter()
if err := router.Merge(userRouter, adminRouter); err != nil {
    log.Fatal(err) // duplicate procedure path
}
// or: router, err := trpcgo.MergeRouters(userRouter, adminRouter)

How It Works

trpcgo implements the tRPC HTTP protocol in Go and provides 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 WithDev(true) with 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. In production, use go generate pre-build. The watcher only starts in dev mode.

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.

Example

See examples/start-trpc/ for a full working example with a Go server and a TanStack Start frontend using @trpc/client and @trpc/tanstack-react-query.

Compatibility

Go: Requires Go 1.26+ (uses tool directive, errors.AsType, generics).

tRPC client: Works with @trpc/client v11 and @trpc/react-query v11. The generated AppRouter type imports from @trpc/server (which is a dependency of @trpc/client).

HTTP: Pure net/http, no framework dependency. Works with any Go router or middleware.

CORS: trpcgo does not handle CORS. Use middleware from your HTTP router or a dedicated package (e.g. rs/cors).

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:

r := trpcgo.NewRouter(trpcgo.WithDev(true), trpcgo.WithTypeOutput("gen/trpc.ts"))
defer r.Close()

trpcgo.Query(r, "user.get", func(ctx context.Context, input GetUserInput) (User, error) {
	return User{ID: input.ID, Name: "Alice"}, nil
})

// import "github.com/befabri/trpcgo/trpc"
http.Handle("/trpc/", trpc.NewHandler(r, "/trpc"))

Procedures

Six registration functions cover all tRPC procedure types — each returns an error if the path is already registered:

Must* variants (MustQuery, MustVoidQuery, MustMutation, MustVoidMutation, MustSubscribe, MustVoidSubscribe) panic on duplicate registration and are the idiomatic choice for application bootstrap code.

Use Procedure to create a reusable base procedure that bundles middleware and metadata — the Go equivalent of tRPC's composable procedure builder:

authedProcedure := trpcgo.Procedure().Use(authMiddleware)
adminProcedure  := authedProcedure.Use(adminCheck).WithMeta(roleMeta{})

trpcgo.MustQuery(r, "user.list", listUsers, authedProcedure)
trpcgo.MustMutation(r, "admin.ban", banUser, adminProcedure)

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. Access procedure metadata with GetProcedureMeta or the typed GetMeta. 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 20 standard tRPC error codes are provided as constants (e.g. CodeNotFound, CodeUnauthorized, CodeTooManyRequests). Use HTTPStatusFromCode and NameFromCode for code conversions.

Merging and Lifecycle

Combine procedures from multiple routers with MergeRouters or Router.Merge. Call Router.Close to stop the file watcher on shutdown.

Server-Side Calls

Invoke procedures from Go without HTTP using Call (typed) or Router.RawCall (untyped). Both run the full middleware chain.

See the project README for full documentation, frontend setup guides, struct tag reference, and working examples: https://github.com/befabri/trpcgo

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyResponseMetadata added in v0.9.0

func ApplyResponseMetadata(ctx context.Context, w http.ResponseWriter)

ApplyResponseMetadata writes accumulated cookies and headers from SetCookie and SetResponseHeader calls to the ResponseWriter. Must be called before WriteHeader.

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 GetMeta added in v0.6.1

func GetMeta[T any](ctx context.Context) (T, bool)

GetMeta extracts typed metadata from the procedure context. Returns the zero value and false if the context has no procedure metadata or if the metadata is not of type T.

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 IsStreamResult added in v0.9.0

func IsStreamResult(result any) bool

IsStreamResult reports whether a procedure result is a subscription stream. Protocol handler packages use this to detect streams and switch to SSE handling.

func MustMutation added in v0.7.0

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

MustMutation is like Mutation but panics if registration fails.

func MustQuery added in v0.7.0

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

MustQuery is like Query but panics if registration fails. Use in application bootstrap code when a registration error is a programmer mistake.

func MustSubscribe added in v0.7.0

func MustSubscribe[I any, O any](r *Router, path string, fn func(ctx context.Context, input I) (<-chan O, error), opts ...ProcedureOption)

MustSubscribe is like Subscribe but panics if registration fails.

func MustSubscribeWithFinal added in v0.9.0

func MustSubscribeWithFinal[I any, O any](r *Router, path string, fn func(ctx context.Context, input I) (<-chan O, func() any, error), opts ...ProcedureOption)

MustSubscribeWithFinal is like SubscribeWithFinal but panics if registration fails.

func MustVoidMutation added in v0.7.0

func MustVoidMutation[O any](r *Router, path string, fn func(ctx context.Context) (O, error), opts ...ProcedureOption)

MustVoidMutation is like VoidMutation but panics if registration fails.

func MustVoidQuery added in v0.7.0

func MustVoidQuery[O any](r *Router, path string, fn func(ctx context.Context) (O, error), opts ...ProcedureOption)

MustVoidQuery is like VoidQuery but panics if registration fails.

func MustVoidSubscribe added in v0.7.0

func MustVoidSubscribe[O any](r *Router, path string, fn func(ctx context.Context) (<-chan O, error), opts ...ProcedureOption)

MustVoidSubscribe is like VoidSubscribe but panics if registration fails.

func MustVoidSubscribeWithFinal added in v0.9.0

func MustVoidSubscribeWithFinal[O any](r *Router, path string, fn func(ctx context.Context) (<-chan O, func() any, error), opts ...ProcedureOption)

MustVoidSubscribeWithFinal is like VoidSubscribeWithFinal but panics if registration fails.

func Mutation

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

Mutation registers a mutation procedure. Returns an error if path is already registered.

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 NewResultEnvelope added in v0.9.0

func NewResultEnvelope(data any) any

NewResultEnvelope creates the tRPC success response envelope: {"result":{"data":...}}. Protocol handler packages use this to format responses.

func Query

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

Query registers a query procedure. Returns an error if path is already registered.

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) error

Subscribe registers a subscription procedure. Returns an error if path is already registered.

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 SubscribeWithFinal added in v0.9.0

func SubscribeWithFinal[I any, O any](r *Router, path string, fn func(ctx context.Context, input I) (<-chan O, func() any, error), opts ...ProcedureOption) error

SubscribeWithFinal registers a subscription whose handler returns a final value function. The function is called when the channel closes and its return value is sent in the SSE done event.

func VoidMutation

func VoidMutation[O any](r *Router, path string, fn func(ctx context.Context) (O, error), opts ...ProcedureOption) error

VoidMutation registers a mutation procedure with no input. Returns an error if path is already registered.

func VoidQuery

func VoidQuery[O any](r *Router, path string, fn func(ctx context.Context) (O, error), opts ...ProcedureOption) error

VoidQuery registers a query procedure with no input. Returns an error if path is already registered.

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) error

VoidSubscribe registers a subscription procedure with no input. Returns an error if path is already registered.

func VoidSubscribeWithFinal added in v0.9.0

func VoidSubscribeWithFinal[O any](r *Router, path string, fn func(ctx context.Context) (<-chan O, func() any, error), opts ...ProcedureOption) error

VoidSubscribeWithFinal registers a subscription with no input whose handler returns a final value function. The function is called when the channel closes and its return value is sent in the SSE done event.

func WithProcedureMeta added in v0.9.0

func WithProcedureMeta(ctx context.Context, pm ProcedureMeta) context.Context

WithProcedureMeta injects procedure metadata into the context. Protocol handler packages call this before executing a procedure so middleware can access metadata via GetProcedureMeta.

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 SanitizeError added in v0.9.0

func SanitizeError(err error) *Error

SanitizeError converts an arbitrary error to a client-safe *Error. If the error is already a *Error, internal details are stripped. Non-tRPC errors are replaced with a generic INTERNAL_SERVER_ERROR.

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 ErrorEnvelope added in v0.6.1

type ErrorEnvelope struct {
	Error ErrorShape `json:"error"`
}

ErrorEnvelope is the error response envelope following JSON-RPC 2.0 conventions. It is exposed so custom error formatters can inspect or extend the default shape.

func DefaultErrorEnvelope added in v0.9.0

func DefaultErrorEnvelope(err *Error, path string, isDev bool) ErrorEnvelope

DefaultErrorEnvelope builds the standard tRPC error envelope for an error. Protocol handler packages use this to format error responses.

type ErrorFormatterInput

type ErrorFormatterInput struct {
	Error *Error
	Type  ProcedureType
	Path  string
	Input json.RawMessage // raw JSON input; nil for pre-execution errors
	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.

Security: Ctx carries the full request context, which may contain auth tokens or other sensitive values. Avoid including context values in formatted error responses.

type ErrorShape added in v0.6.1

type ErrorShape struct {
	Code    ErrorCode      `json:"code"`
	Message string         `json:"message"`
	Data    ErrorShapeData `json:"data"`
}

ErrorShape is the error object within an ErrorEnvelope.

type ErrorShapeData added in v0.6.1

type ErrorShapeData struct {
	Code       string `json:"code"`
	HTTPStatus int    `json:"httpStatus"`
	Path       string `json:"path,omitempty"`
	Stack      string `json:"stack,omitempty"`
}

ErrorShapeData contains machine-readable error metadata.

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(ctx context.Context, r *http.Request) context.Context) Option

WithContextCreator sets a function that creates the base context for each request. The ctx argument is the request's existing context (r.Context()), so values and cancellation propagate automatically when the returned context is derived from it.

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 WithSSEMaxConnections added in v0.6.1

func WithSSEMaxConnections(n int) Option

WithSSEMaxConnections sets the maximum number of concurrent SSE subscriptions. When the limit is reached, new subscription requests are rejected with a TOO_MANY_REQUESTS (429) error. Default is 0 (unlimited). Set to -1 to explicitly disable the limit. Passing 0 keeps the default.

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 30 minutes. Set to -1 for unlimited. Passing 0 keeps the default.

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. The path specifies where the TypeScript AppRouter type file is written. 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 WithWatchPackages added in v0.7.0

func WithWatchPackages(patterns ...string) Option

WithWatchPackages restricts dev watcher + static regeneration to the given Go package patterns (go/packages syntax, e.g. "./cmd/api", "./internal/...").

When unset, the watcher auto-detects Go directories under the working directory. This option is only used by the dev watcher path (WithDev + WithTypeOutput).

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 ProcedureBuilder added in v0.7.1

type ProcedureBuilder struct {
	// contains filtered or unexported fields
}

ProcedureBuilder is a reusable base procedure that accumulates middleware and metadata. It is immutable: every chain method returns a new instance. A *ProcedureBuilder satisfies ProcedureOption and can be passed directly to any registration function.

Usage:

authedProcedure := trpcgo.Procedure().Use(authMiddleware)
adminProcedure  := authedProcedure.Use(adminCheck).WithMeta(roleMeta{})

trpcgo.MustQuery(router, "user.list", listUsers, authedProcedure)
trpcgo.MustMutation(router, "admin.ban", banUser, adminProcedure)

func Procedure added in v0.7.1

func Procedure(base ...ProcedureOption) *ProcedureBuilder

Procedure creates a new ProcedureBuilder, optionally pre-seeded with existing options or other builders.

func (*ProcedureBuilder) Use added in v0.7.1

Use returns a new ProcedureBuilder with the given middleware appended. The receiver is not modified.

func (*ProcedureBuilder) With added in v0.8.0

With returns a new ProcedureBuilder with the given options appended. Unlike Use (middleware-only), With accepts any ProcedureOption, including OutputValidator and OutputParser. The receiver is not modified.

[trpcgo generate] can discover OutputParser calls passed directly to With. WithOutputParser (untyped) degrades codegen to unknown — use a typed OutputParser when the output type changes.

func (*ProcedureBuilder) WithMeta added in v0.7.1

func (b *ProcedureBuilder) WithMeta(meta any) *ProcedureBuilder

WithMeta returns a new ProcedureBuilder with the metadata set. The receiver is not modified.

func (*ProcedureBuilder) WithOutputParser added in v0.8.0

func (b *ProcedureBuilder) WithOutputParser(fn func(any) (any, error)) *ProcedureBuilder

WithOutputParser returns a new ProcedureBuilder with an untyped output parser set. The receiver is not modified. Generated output types fall back to unknown unless a typed OutputParser is also present.

func (*ProcedureBuilder) WithOutputValidator added in v0.8.0

func (b *ProcedureBuilder) WithOutputValidator(fn func(any) error) *ProcedureBuilder

WithOutputValidator returns a new ProcedureBuilder with an untyped output validator set. The receiver is not modified.

type ProcedureEntry added in v0.9.0

type ProcedureEntry struct {
	// contains filtered or unexported fields
}

ProcedureEntry is a read-only handle to a registered procedure with a pre-computed middleware chain. Obtained from ProcedureMap. Safe for concurrent use.

func (*ProcedureEntry) InputType added in v0.9.0

func (e *ProcedureEntry) InputType() reflect.Type

InputType returns the Go input type, or nil for void procedures.

func (*ProcedureEntry) Meta added in v0.9.0

func (e *ProcedureEntry) Meta() any

Meta returns the user-defined metadata attached via WithMeta.

func (*ProcedureEntry) OutputType added in v0.9.0

func (e *ProcedureEntry) OutputType() reflect.Type

OutputType returns the Go output type.

func (*ProcedureEntry) Type added in v0.9.0

func (e *ProcedureEntry) Type() ProcedureType

Type returns the procedure type (query, mutation, subscription).

type ProcedureMap added in v0.9.0

type ProcedureMap struct {
	// contains filtered or unexported fields
}

ProcedureMap is a frozen snapshot of registered procedures with pre-computed middleware chains. Safe for concurrent use without locking.

Protocol handler packages use this to build HTTP handlers that serve procedures over the tRPC wire format.

func (*ProcedureMap) All added in v0.9.0

All returns an iterator over all registered procedures. Iteration order is not guaranteed.

func (*ProcedureMap) Len added in v0.9.0

func (pm *ProcedureMap) Len() int

Len returns the number of registered procedures.

func (*ProcedureMap) Lookup added in v0.9.0

func (pm *ProcedureMap) Lookup(path string) (*ProcedureEntry, bool)

Lookup returns the procedure entry for the given path.

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 interface {
	// contains filtered or unexported methods
}

ProcedureOption configures a single procedure registration. Implement this interface via Use, WithMeta, or Procedure.

func OutputParser added in v0.8.0

func OutputParser[O, P any](fn func(O) (P, error)) ProcedureOption

OutputParser creates a typed per-procedure output parser. The function receives the exact output type O and returns a value of type P to send to the client. It can validate (O == P, return unchanged), transform (return a reshaped value), or sanitize (strip fields). If the parser returns an error the client receives an INTERNAL_SERVER_ERROR.

Codegen is aware of the [O, P] type pair: both Router.GenerateTS (reflection) and [trpcgo generate] (static analysis) emit P as the TypeScript output type, keeping the generated contract in sync with what the client actually receives. WithOutputParser (untyped) degrades codegen to unknown because the exact post-parse shape is not statically knowable — use this typed form when the output type changes.

For subscriptions where O = TrackedEvent[T], the parser receives the full TrackedEvent so it can inspect the ID and payload together. If the returned value also implements TrackedEvent its ID is propagated to the SSE stream; otherwise the item is sent without an ID.

The type assertion is checked at runtime: if the output value cannot be asserted to O the parser returns INTERNAL_SERVER_ERROR rather than panicking.

func OutputValidator added in v0.8.0

func OutputValidator[O any](fn func(O) error) ProcedureOption

OutputValidator creates a typed per-procedure output validator. The function receives the exact output type O and returns an error if the output is invalid. It cannot transform the value and does not affect generated output types. If the validator returns an error the client receives an INTERNAL_SERVER_ERROR.

For subscriptions where O = TrackedEvent[T], the validator receives the full TrackedEvent before unwrapping, so the validator type should also be TrackedEvent[T]. The type assertion is checked at runtime: if the output value cannot be asserted to O the validator returns INTERNAL_SERVER_ERROR rather than panicking.

func Use

func Use(mw ...Middleware) ProcedureOption

Use adds per-procedure middleware. It can be passed directly to any registration function or to Procedure when building a base procedure.

func WithMeta

func WithMeta(meta any) ProcedureOption

WithMeta attaches metadata to a procedure, accessible in middleware via GetProcedureMeta.

func WithOutputParser added in v0.8.0

func WithOutputParser(fn func(any) (any, error)) ProcedureOption

WithOutputParser sets a per-procedure output parser. The parser is called with the handler's return value after a successful handler call. It can validate, transform, or sanitize the output — the value it returns is what gets sent to the client. If the parser returns an error, the client receives an INTERNAL_SERVER_ERROR. For subscriptions, the parser runs on each emitted item.

Use OutputParser for a typed alternative. Because the parser takes and returns any, generated types fall back to unknown unless a typed OutputParser override is also present.

func WithOutputValidator added in v0.8.0

func WithOutputValidator(fn func(any) error) ProcedureOption

WithOutputValidator sets a per-procedure output validator. The validator is called with the handler's return value after a successful handler call. It may reject invalid outputs but cannot transform them. If the validator returns an error, the client receives an INTERNAL_SERVER_ERROR. For subscriptions, the validator runs on each emitted item before any output parser.

Use OutputValidator for a typed alternative.

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. Use [trpc.NewHandler] from the protocol sub-package to serve them over HTTP.

func MergeRouters

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

MergeRouters creates a new Router combining procedures from all sources. Returns an error 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, err := trpcgo.MergeRouters(users, posts)
if err != nil {
	fmt.Println(err)
	return
}
_ = trpc.NewHandler(app, "/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) AllowBatching added in v0.9.0

func (r *Router) AllowBatching() bool

AllowBatching reports whether batch requests are enabled.

func (*Router) AllowMethodOverride added in v0.9.0

func (r *Router) AllowMethodOverride() bool

AllowMethodOverride reports whether queries can be sent as POST.

func (*Router) BuildProcedureMap added in v0.9.0

func (r *Router) BuildProcedureMap() *ProcedureMap

BuildProcedureMap creates a frozen snapshot of all registered procedures with pre-computed middleware chains. Protocol handler packages call this once during handler construction.

The snapshot is independent of the Router — subsequent registrations or middleware additions do not affect it.

func (*Router) Close added in v0.6.1

func (r *Router) Close() error

Close stops the file watcher goroutine (if running) and releases resources. Safe to call multiple times.

func (*Router) ContextCreator added in v0.9.0

func (r *Router) ContextCreator() func(context.Context, *http.Request) context.Context

ContextCreator returns the user-supplied context creator function, or nil.

func (*Router) ErrorCallback added in v0.9.0

func (r *Router) ErrorCallback() func(context.Context, *Error, string)

ErrorCallback returns the user-supplied error callback, or nil.

func (*Router) ErrorFormatter added in v0.9.0

func (r *Router) ErrorFormatter() func(ErrorFormatterInput) any

ErrorFormatter returns the user-supplied error formatter function, or nil.

func (*Router) ExecuteEntry added in v0.9.0

func (r *Router) ExecuteEntry(ctx context.Context, entry *ProcedureEntry, raw json.RawMessage) (any, error)

ExecuteEntry decodes raw JSON input, validates it, and runs the procedure's handler chain. This is the protocol-agnostic execution path for use by handler packages that implement their own HTTP wire format.

The returned result may be a stream (for subscription procedures). Use IsStreamResult to check and ConsumeStream to read items.

func (*Router) FormatError added in v0.9.0

func (r *Router) FormatError(err *Error, path string, input json.RawMessage, ctx context.Context, typ ProcedureType) any

FormatError builds the error response, applying the custom error formatter if one is configured on the router. Protocol handler packages use this to produce error responses that respect the user's error formatter.

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) IsDev added in v0.9.0

func (r *Router) IsDev() bool

IsDev reports whether the router is in development mode.

func (*Router) MaxBatchSize added in v0.9.0

func (r *Router) MaxBatchSize() int

MaxBatchSize returns the maximum batch size. Returns 0 for unlimited.

func (*Router) MaxBodySize added in v0.9.0

func (r *Router) MaxBodySize() int64

MaxBodySize returns the configured maximum request body size in bytes. Returns 0 for unlimited.

func (*Router) MaxSSEConnections added in v0.9.0

func (r *Router) MaxSSEConnections() int

MaxSSEConnections returns the configured SSE connection limit. Returns 0 for unlimited.

func (*Router) Merge

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

Merge copies all procedures from the source routers into this router. Returns an error if any procedure path already exists. The operation is atomic: on error, no procedures are added. 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) SSEMaxDuration added in v0.9.0

func (r *Router) SSEMaxDuration() time.Duration

SSEMaxDuration returns the configured SSE maximum duration. Returns 0 for unlimited.

func (*Router) SSEPingInterval added in v0.9.0

func (r *Router) SSEPingInterval() time.Duration

SSEPingInterval returns the configured SSE keep-alive ping interval.

func (*Router) SSEReconnectAfterInactivityMs added in v0.9.0

func (r *Router) SSEReconnectAfterInactivityMs() int

SSEReconnectAfterInactivityMs returns the configured reconnect-after-inactivity value in milliseconds sent to SSE clients.

func (*Router) StartDevWatcher added in v0.9.0

func (r *Router) StartDevWatcher()

StartDevWatcher starts the file watcher if dev mode and type generation are configured. Called by trpc.NewHandler during construction.

func (*Router) StrictInput added in v0.9.0

func (r *Router) StrictInput() bool

StrictInput reports whether strict input parsing is enabled.

func (*Router) TrackSSEConnection added in v0.9.0

func (r *Router) TrackSSEConnection(delta int64) int64

TrackSSEConnection atomically adjusts the SSE connection count by delta and returns the new count. Protocol handlers use this to enforce connection limits.

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

func (*Router) Validator added in v0.9.0

func (r *Router) Validator() func(any) error

Validator returns the user-supplied input validator function, or nil.

type StreamConsumer added in v0.9.0

type StreamConsumer struct {
	// contains filtered or unexported fields
}

StreamConsumer provides protocol-agnostic access to a subscription stream. Protocol handlers use it to read items and write SSE events in their own wire format. Create one via ConsumeStream.

func ConsumeStream added in v0.9.0

func ConsumeStream(result any) *StreamConsumer

ConsumeStream extracts a StreamConsumer from a streaming procedure result. Returns nil if the result is not a stream. The consumer reads items from the underlying channel with output validation/parsing applied.

The stream should be consumed by exactly one reader.

func (*StreamConsumer) Recv added in v0.9.0

func (sc *StreamConsumer) Recv(ctx context.Context) (data any, id string, retry int, err error)

Recv returns the next item from the stream. It blocks until an item is available, the stream is closed, or the context is cancelled.

On success, data is the payload (with TrackedEvent unwrapped), id is the event ID (empty if not tracked), and retry is the reconnect interval in milliseconds (0 if not set). Returns io.EOF when the stream ends — data may be non-nil on EOF if the stream has a final return value.

type TrackedEvent

type TrackedEvent[T any] struct {
	ID    string
	Retry int // milliseconds; 0 means not set
	Data  T
}

TrackedEvent wraps a value with an ID and optional retry interval for SSE. 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. Retry tells the client how many milliseconds to wait before reconnecting (0 means not set).

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.
Package trpc provides an HTTP handler that serves trpcgo procedures using the tRPC wire format.
Package trpc provides an HTTP handler that serves trpcgo procedures using the tRPC wire format.

Jump to

Keyboard shortcuts

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