Skip to content

Router & Options

Router owns procedure registrations and runtime configuration.

router := trpcgo.NewRouter(
trpcgo.WithBatching(true),
trpcgo.WithStrictInput(true),
trpcgo.WithValidator(validate.Struct),
)
defer router.Close()

Use the trpc package to serve a router over the tRPC HTTP protocol:

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.

OptionDefaultBehavior
WithBatching(bool)trueEnables tRPC batch requests with ?batch=1.
WithMethodOverride(bool)falseAllows queries to be called with POST.
WithMaxBodySize(n)1 MiBLimits POST bodies and GET input query values. -1 disables the limit.
WithMaxBatchSize(n)10Limits procedures in one batch. -1 disables the limit.
WithStrictInput(bool)trueRejects unknown JSON object fields and trailing JSON tokens.

Strict input uses Go’s json.Decoder.DisallowUnknownFields and is enabled by default. Unknown object fields are returned as BAD_REQUEST; malformed JSON and trailing JSON tokens are returned as parse errors. Set WithStrictInput(false) only when you intentionally want Go’s normal json.Unmarshal behavior, which ignores unknown object fields.

trpc.NewHandler accepts options for HTTP-layer behavior:

handler := trpc.NewHandler(router, "/trpc",
trpc.WithCORS(trpc.CORSConfig{
AllowedOrigins: []string{"https://app.example.com"},
AllowCredentials: true,
}),
trpc.WithTrustedOrigins("https://app.example.com"),
)
OptionDefaultBehavior
trpc.WithContentTypeEnforcement(bool)trueRequires Content-Type: application/json for POST requests with bodies.
trpc.WithCSRFProtection(bool)trueRejects cross-origin POST requests unless the Origin or Referer is same-origin or trusted.
trpc.WithCSRFRequireOrigin(bool)falseRejects all POST requests that lack both Origin and Referer. Cookie-bearing POSTs are rejected without those headers even when this is false.
trpc.WithSubscriptionOriginCheck(bool)falseRejects browser subscription requests whose Origin or Referer is not same-origin, trusted, public, or CORS-allowed. Cookie-bearing subscriptions without either header are rejected.
trpc.WithPublicOrigin(origin) / trpc.WithPublicOrigins(origins...)noneTreats exact public API origins as same-origin for deployments behind TLS-terminating proxies.
trpc.WithTrustedOrigins(origins...)noneAdds exact scheme+host origins that may send cross-origin POST requests.
trpc.WithCORS(config)disabledHandles CORS preflights and response headers for configured origins. CORS origins do not grant CSRF trust.

Handler options are separate from router options because they depend on HTTP deployment details. WithCORS accepts exact origins such as https://app.example.com; wildcard CORS (*) can emit Access-Control-Allow-Origin: * when credentials are disabled, but it is not trusted by the CSRF check. For cross-origin browser mutations, configure both CORS read access with WithCORS and POST trust with WithTrustedOrigins.

WithSubscriptionOriginCheck(true) is opt-in: it gates GET/SSE subscriptions before their resolvers run, accepting same-origin requests and origins from WithTrustedOrigins, WithPublicOrigin, or WithCORS. POST subscriptions go through the normal CSRF check first.

CORSConfig.AllowedHeaders replaces the default allow-list. The default is Authorization, Content-Type, Last-Event-Id, and trpc-accept; include those headers when you add custom headers and still need auth, tRPC JSONL, or subscription resume support.

Configured origins must be exact http or https scheme+host values with no path, query, fragment, or user info. Header values such as Referer may include a path; trpcgo extracts their origin before comparison.

WithValidator runs after JSON decoding and only for struct-typed inputs.

validate := validator.New()
router := trpcgo.NewRouter(
trpcgo.WithValidator(validate.Struct),
)

validate tags do not run at runtime unless you configure this option.

OptionBehavior
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:

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.

OptionDefaultBehavior
WithSSEPingInterval(d)10sSends keep-alive ping events.
WithSSEMaxDuration(d)30mCloses streams with a return event after the duration. -1 means unlimited.
WithSSEReconnectAfterInactivity(d)disabledSends reconnectAfterInactivityMs in the connected event.
WithSSEMaxConnections(n)unlimitedRejects extra streams with TOO_MANY_REQUESTS.
OptionBehavior
WithTypeOutput(path)Writes generated TypeScript in dev mode.
WithZodOutput(path)Writes generated Zod schemas in dev mode.
WithZodMini(bool)Uses zod/mini functional syntax.
WithEnumsOutput(path)Writes runtime enum value objects in dev mode.
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.

Use router merging to split registrations across packages.

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.