Surface¶
This chapter prescribes the public surface of a Go service: the
RPC framework, the router and interceptor chain, error mapping,
pagination, authentication, and the end-to-end use of protobuf
enums. It also prescribes the canonical frontend stack, including
the canonical routing library and a clearly labeled alternative.
The RFC 2119 keywords used here (MUST, MUST NOT, SHOULD,
SHOULD NOT, MAY) carry the senses defined in chapter
00-introduction.md. The handler.go layer that hosts these
prescriptions is defined in chapter 01-architecture.md; the
tenant-scoping invariants the authorization interceptor enforces
at the edge are defined in chapter 02-data.md.
TL;DR¶
- The canonical RPC framework is Connect-RPC, mounted on the
chiHTTP router. REST endpoints MAY coexist on the same chi router for browser callbacks, health probes, and metrics. - Errors MUST be produced through a single
apiutilhelper package that maps domain errors toconnect.Codevalues so that handler code never constructs error responses inline. - Authentication SHOULD use a short-lived bearer JWT delivered to
the browser in an
HttpOnly; Secure; SameSite=Laxcookie. - Protobuf enums MUST be the source of truth from wire to database. Conversion to database strings MUST happen at a boundary package; domain code MUST work in the typed enum form.
- The canonical frontend routing library is react-router v7 in
Data Mode (
createBrowserRouter). TanStack Router v1 is a clearly labeled alternative when end-to-end type safety dominates other concerns. The react-router v8 release is imminent (~June 2026); new projects SHOULD monitor for the upgrade and plan migration time.
Why this choice¶
- Connect-RPC offers a typed RPC contract with HTTP / JSON and gRPC-Web transports, browser-native streaming, and code generation for Go and TypeScript. It avoids gRPC's browser-environment awkwardness without giving up types.
- chi is a minimal, idiomatic Go HTTP router that composes
cleanly with Connect-RPC handlers and accepts standard
http.Handlermiddleware. - A single
apiutilpackage centralizes the domain-error-to-connect.Codemapping. Handlers that build their own error responses drift over time; a single helper surface holds the line and makes audit trivial. - JWT in HttpOnly cookies keeps tokens out of JavaScript reach (mitigating XSS exfiltration) while preserving the stateless properties of bearer auth.
- Proto enums end to end prevents the classic drift where a database string column and a proto enum disagree about which values are legal.
- react-router v7 Data Mode is the canonical recommendation because Data Mode unlocks route-level loaders, actions, and pending UI without prescribing a server runtime. TanStack Router earns alternative status: its end-to-end type inference is stronger, but the type-system depth has a real onboarding cost.
Prescriptive guidance¶
RPC framework and router¶
- The HTTP entry point MUST be a
chirouter constructed in the composition root. - Connect-RPC service handlers MUST be mounted on the chi router
via the generated
New<Service>Handlerfunctions emitted byprotoc-gen-connect-go. - REST endpoints (OAuth callbacks, health probes, Prometheus metrics) MAY be mounted on the same chi router; they MUST live under prefix paths that do not collide with the RPC service prefix.
- The router MUST set request timeouts and a sane body-size limit before any handler runs.
Interceptor chain ordering¶
The Connect-RPC interceptor chain MUST be ordered so that cross-cutting concerns observe and act on the request in a predictable sequence:
- Recovery / panic capture — outermost so it observes panics from every later interceptor.
- Request-id and tracing — assigns a request identifier and establishes the OpenTelemetry span before logs run.
- Structured logging — emits request-start and request-end log lines correlated to the trace and (once auth has run) the principal.
- Authentication — extracts the bearer token, validates the JWT, and attaches the principal to the context.
- Authorization — checks tenant scoping and permission policy against the principal from step 4.
- Validation — schema validation of the request message.
- Business handler — innermost.
flowchart LR
Client[Connect-RPC client] --> Recover[Recovery]
Recover --> Trace[Request-id + tracing]
Trace --> Log[Structured logging]
Log --> AuthN[Authentication]
AuthN --> AuthZ[Authorization]
AuthZ --> Validate[Validation]
Validate --> Handler[Business handler]
Handler --> Domain[Domain service]
Error mapping with apiutil¶
- Domain code MUST return typed errors (sentinel values such as
var ErrNotFound = errors.New("not found")or wrapping equivalents). - Handlers MUST translate domain errors via an
apiutilhelper package located at<repo>/internal/api/apiutil/. Handlers MUST NOT construct*connect.Errorvalues inline. - The helper MUST map a closed set of domain errors to
connect.Codevalues (CodeNotFound,CodeInvalidArgument,CodeAlreadyExists, etc.) and MUST default unmapped errors toCodeInternalwith the original error preserved as the cause for logs. - The helper MUST scrub internal error messages from the
response body for
CodeInternalerrors; the original message MAY be logged but MUST NOT be returned to the client.
Pagination¶
- List RPCs MUST accept an opaque
page_token(string) and apage_size(int32 with a server-enforced maximum). - List RPCs MUST return a
next_page_token(string, empty when no further pages exist). - Page tokens MUST be opaque to the client (typically a
base64-encoded cursor over an indexable column such as
(created_at, id)). - List handlers MUST cap
page_sizeto a sane upper bound (for example, 200) regardless of client request, and SHOULD return the effective value back to the client in the response.
Authentication¶
- Browser clients SHOULD authenticate via a short-lived bearer
JWT delivered in an
HttpOnly; Secure; SameSite=Laxcookie. - Native and service-to-service clients SHOULD use the
Authorization: Bearer <jwt>header. - Tokens MUST carry an
expclaim no further than 15 minutes out; refresh MUST be handled by a separate refresh-token endpoint, with the refresh token stored in its own HttpOnly cookie. - Token validation MUST happen in the authentication interceptor; downstream handlers MUST read the validated principal from the request context rather than re-parsing the token.
- Service-to-service tokens MUST be validated against the same signer set as user tokens; a separate signer MAY be used but MUST be enumerated in the same configuration block.
Proto enums end to end¶
- All categorical fields (status, kind, role, phase) MUST be proto enums in the wire definition.
- Database columns that mirror proto enums MUST use
text(orvarchar) and MUST be guarded by aCHECKconstraint listing the legal values, or by a foreign-key constraint into a reference table. - Conversion between the proto enum and the database string MUST
happen in a dedicated boundary package (for example,
<repo>/internal/<domain>/enumconv/). Domain code MUST work in the typed enum form exclusively. - Adding a new enum value MUST be a coordinated migration: the
proto definition MUST add the value before the database
CHECKconstraint admits it; the database constraint MUST admit the value before any handler returns it.
Frontend stack¶
- The frontend SHOULD use React 19 with TypeScript (strict mode) and Vite as the build tool.
- The Connect-Web client MUST be generated from the same proto
definitions as the server. A single
connect.tshelper SHOULD construct the transport, attach the bearer-token interceptor, and implement opaque refresh-token rotation. - Provider composition (query client, router, theme, auth) MUST
live in a single top-level
App.tsx(or equivalent) so the dependency order is auditable in one file.
Routing (canonical): react-router v7 Data Mode¶
- The canonical routing library is react-router v7 used in
Data Mode, configured with
createBrowserRouter. - Routes MUST be authored in a centralized
routes.tsxmodule usingcreateBrowserRouterso that data-loading APIs (loader,action,useLoaderData,useNavigation) are available.
// <repo>/web/src/routes.tsx
import { createBrowserRouter, RouterProvider } from "react-router";
import { AppLayout } from "./layouts/AppLayout";
import { DashboardPage, dashboardLoader } from "./pages/Dashboard";
import { LoginPage, loginAction } from "./pages/Login";
export const router = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
children: [
{
index: true,
loader: dashboardLoader,
element: <DashboardPage />,
},
{
path: "login",
action: loginAction,
element: <LoginPage />,
},
],
},
]);
// in App.tsx:
// <RouterProvider router={router} />
- Loaders MUST handle authentication redirects centrally; individual page components SHOULD NOT replicate redirect-to-login logic.
- Pending UI MUST use
useNavigation()rather than per-page loading flags; this gives consistent transition behavior across routes.
Forward-looking: react-router v8 (~June 2026)
react-router v8 is targeted for release in approximately
June 2026. New projects scaffolding today SHOULD monitor the
react-router release notes and budget migration time within
the first six months of v8 GA. The Data Mode APIs in v7
(createBrowserRouter, loader, action) are expected to
carry forward, but new projects SHOULD avoid v7-only patterns
that the v7 release notes explicitly mark as transitional.
Alternative: TanStack Router v1¶
- TanStack Router v1 is the clearly labeled alternative when end-to-end type safety dominates other concerns.
- TanStack Router provides fully typed route params, typed search params, and inferred loader return types across the route tree, surfaced as compile-time errors when a route is used incorrectly.
- The trade-off: the type-system depth has a real onboarding cost. New contributors MUST learn the router's typed conventions before they can productively add routes. Teams that do not already operate under a strict-TypeScript culture SHOULD prefer the canonical react-router recommendation above.
- TanStack Router v1 is verified stable as of the 2026-05-08 snapshot date. Adopting orgs MAY adopt it as the canonical router for projects where typed-route benefits justify the onboarding cost; the decision MUST be recorded as an ADR.
Reference Implementation: Pioneer
The surface prescriptions are exercised end to end in the canonical reference implementation:
pioneer/internal/server/router.goshows the chi router plus the Connect-RPC interceptor chain assembled in the prescribed order (recovery → request-id / tracing → structured logging → authentication → authorization → validation → handler).pioneer/internal/api/apiutil/errors.gois the centralized error-mapping helper; handler code calls into it rather than constructing*connect.Errorvalues inline.pioneer/proto/pioneer/v1/common.protois the source of truth for shared enums; database columns carry the same values astextwith aCHECKconstraint, and the conversion happens at a dedicated boundary package.pioneer/web/src/App.tsxshows the provider chain (query client, router, theme, auth) composed at the top of the app in a single auditable file.pioneer/web/src/routes.tsxshows the canonicalcreateBrowserRouterconfiguration with route-level loaders, actions, and anAppLayoutwrapping authenticated pages.pioneer/web/src/lib/connect.tsshows the Connect-Web transport construction, the bearer-token interceptor, and opaque refresh-token rotation.
Pinned versions (snapshot 2026-05-08)¶
| Component | Version | Notes |
|---|---|---|
| Connect-RPC (Go) | v1.18.x | connectrpc.com/connect |
| chi | v5.x | Standard http.Handler middleware |
| React | 19.2.x | Concurrent rendering stable |
| Vite | 7.3.x | Build tool |
| TypeScript | ~5.9.x | Strict mode required |
| react-router | v7.x Data Mode | Canonical (v8 ~June 2026) |
| TanStack Router | v1.x | Alternative for type-safe routing |
| @connectrpc/connect-web | v1.x | Browser transport |
| lucide-react | ^0.577.x | Icon set |
Adopting orgs MUST validate these versions against their own
toolchain constraints at adoption time. The snapshot date above
matches last-reviewed in this chapter's frontmatter.
Pitfalls¶
- Interceptor order inversions. Putting authentication outside recovery loses panic context; putting structured logging before request-id and tracing produces logs without span correlation. The order specified in the Interceptor chain ordering section is load-bearing and MUST be preserved.
- Inline
*connect.Errorconstruction. Handlers that hand-roll error responses produce inconsistent codes across the service. Errors MUST flow through theapiutilhelper. - JWTs in
localStorageor non-HttpOnly cookies. Tokens reachable from JavaScript are exfiltrated by any XSS vulnerability. Browser tokens MUST live in HttpOnly cookies. - Free-form string enums in the database. A
textcolumn without aCHECKconstraint or without proto-driven conversion drifts away from the wire format. Enums MUST be proto-defined and database-constrained. - Mixing TanStack Router and react-router in the same app. Two routers fight for the URL. Projects MUST choose one and hold the line; cross-router experiments MUST run in a separate workspace.
- Treating the v7 → v8 migration as a "later" concern. v8 is targeted for ~June 2026; teams that have not budgeted migration time MUST do so in the next quarterly review and MUST track v7-only deprecations in their tech-debt register.
- Cross-origin cookies without
SameSitediscipline. A cookie that does not setSameSite=Lax(orSameSite=Strictwhere compatible) opens CSRF avenues. Browser auth cookies MUST set an explicitSameSiteattribute.
See also¶
00-introduction.md— RFC 2119 keyword definitions and doc conventions used throughout this chapter.01-architecture.md— the three-layer pattern that hosts the Connect-RPC handlers prescribed here.02-data.md— the tenant-scoping and soft-delete invariants that the authorization interceptor enforces at the edge.06-security.md— the deeper authentication and authorization treatment (sibling casting).10-typescript.md— the TypeScript discipline that the frontend stack inherits (sibling casting).- Connect-RPC documentation: https://connectrpc.com/docs/
- react-router v7 Data Mode: https://reactrouter.com/start/data/installation
- TanStack Router v1: https://tanstack.com/router/latest
- chi router: https://github.com/go-chi/chi