Skip to content

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 chi HTTP router. REST endpoints MAY coexist on the same chi router for browser callbacks, health probes, and metrics.
  • Errors MUST be produced through a single apiutil helper package that maps domain errors to connect.Code values 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=Lax cookie.
  • 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.Handler middleware.
  • A single apiutil package centralizes the domain-error-to-connect.Code mapping. 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 chi router constructed in the composition root.
  • Connect-RPC service handlers MUST be mounted on the chi router via the generated New<Service>Handler functions emitted by protoc-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:

  1. Recovery / panic capture — outermost so it observes panics from every later interceptor.
  2. Request-id and tracing — assigns a request identifier and establishes the OpenTelemetry span before logs run.
  3. Structured logging — emits request-start and request-end log lines correlated to the trace and (once auth has run) the principal.
  4. Authentication — extracts the bearer token, validates the JWT, and attaches the principal to the context.
  5. Authorization — checks tenant scoping and permission policy against the principal from step 4.
  6. Validation — schema validation of the request message.
  7. 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 apiutil helper package located at <repo>/internal/api/apiutil/. Handlers MUST NOT construct *connect.Error values inline.
  • The helper MUST map a closed set of domain errors to connect.Code values (CodeNotFound, CodeInvalidArgument, CodeAlreadyExists, etc.) and MUST default unmapped errors to CodeInternal with the original error preserved as the cause for logs.
  • The helper MUST scrub internal error messages from the response body for CodeInternal errors; 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 a page_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_size to 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=Lax cookie.
  • Native and service-to-service clients SHOULD use the Authorization: Bearer <jwt> header.
  • Tokens MUST carry an exp claim 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 (or varchar) and MUST be guarded by a CHECK constraint 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 CHECK constraint 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.ts helper 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.tsx module using createBrowserRouter so 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.go shows 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.go is the centralized error-mapping helper; handler code calls into it rather than constructing *connect.Error values inline.
  • pioneer/proto/pioneer/v1/common.proto is the source of truth for shared enums; database columns carry the same values as text with a CHECK constraint, and the conversion happens at a dedicated boundary package.
  • pioneer/web/src/App.tsx shows the provider chain (query client, router, theme, auth) composed at the top of the app in a single auditable file.
  • pioneer/web/src/routes.tsx shows the canonical createBrowserRouter configuration with route-level loaders, actions, and an AppLayout wrapping authenticated pages.
  • pioneer/web/src/lib/connect.ts shows 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.Error construction. Handlers that hand-roll error responses produce inconsistent codes across the service. Errors MUST flow through the apiutil helper.
  • JWTs in localStorage or 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 text column without a CHECK constraint 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 SameSite discipline. A cookie that does not set SameSite=Lax (or SameSite=Strict where compatible) opens CSRF avenues. Browser auth cookies MUST set an explicit SameSite attribute.

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