Skip to content
Back to skills

Connectrpc Architect

ASecurity

ConnectRPC standards — one .proto serving Connect, gRPC and gRPC-Web from a single http.Handler via protoc-gen-connect-go, connect.Code error mapping, the mandatory interceptor chain, streaming caveats (HTTP/2 for client/bidi, gRPC-Web server-stream only), browser callers with no proxy, generic typed clients and conformance testing. Use when designing or reviewing a Connect handler, choosing Connect over vanilla gRPC, or serving proto to browser callers.

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added November 7, 2026
developmentgotestingapibackenddocumentation

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add ralvarez-ai/ralvaskills --skill connectrpc-architect --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Connectrpc Architect?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Connectrpc Architect
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ralvarez-ai-connectrpc-architect/badge)](https://www.skillsdirectory.com/skills/ralvarez-ai-connectrpc-architect)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: connectrpc-architect
version: 1.1.0
description: ConnectRPC standards — one .proto serving Connect, gRPC and gRPC-Web from a single http.Handler via protoc-gen-connect-go, connect.Code error mapping, the mandatory interceptor chain, streaming caveats (HTTP/2 for client/bidi, gRPC-Web server-stream only), browser callers with no proxy, generic typed clients and conformance testing. Use when designing or reviewing a Connect handler, choosing Connect over vanilla gRPC, or serving proto to browser callers.
---

# ConnectRPC Architecture

ConnectRPC (`connectrpc.com/connect`) serves **Connect**, **gRPC** and **gRPC-Web** from one generated `http.Handler` — one `.proto`, no gateway in front of the browser. Pair with [protobuf-architect](../../encoding/protobuf-architect/SKILL.md) for schema design. Go skeletons in [RECIPES.md](RECIPES.md); pinned deps in [STACK.md](STACK.md). Other languages follow the same protocol-level conventions with idiomatic substitutions.

## 1. The value proposition — one handler, three protocols

A single `buf`-generated handler is mounted once and answers all three wire protocols:

| Protocol | Wire | Callers | Notes |
|---|---|---|---|
| **Connect** | `POST` + JSON or Protobuf, HTTP/1.1 or 2 | Any HTTP client, browsers | No proxy; `curl`-able with `-H 'Content-Type: application/json'` |
| **gRPC** | Length-prefixed Protobuf, HTTP/2 | gRPC clients, backend-to-backend | Same `grpc-status` trailers as vanilla gRPC |
| **gRPC-Web** | Protobuf, trailers encoded in the body | Browsers via `@connectrpc/connect-web` | No proxy/gateway required |

**House position:**

- **`.proto` is the RPC contract** — the source of truth for the RPC surface, exactly as for [grpc-architect](../grpc-architect/SKILL.md).
- **Connect is the primary adapter.** New Go services generate the Connect handler and mount it as the RPC entrypoint.
- **Keep a native gRPC adapter from the same proto as deliberate insurance.** Registering the same implementation on a `*grpc.Server` costs one more generated stub; if Connect is ever deprecated, or a pure-gRPC peer appears, the swap is mechanical. That path and the house `grpckit` live in [grpc-architect](../grpc-architect/SKILL.md) and [GRPCKIT](../../personal/go-house-libs/references/GRPCKIT.md).
- **REST is a separate public surface, not generated from proto.** Where the contract is OpenAPI (public, `curl`-first, cache-friendly), follow [rest-api-architect](../rest-api-architect/SKILL.md) — don't transpile proto to REST with `grpc-gateway`. Internal RPC and public REST coexist without a shared type layer.

## 2. Service definition & generated surface

Proto layout, message naming, pagination and `google.protobuf.Empty` rules come from [protobuf-architect](../../encoding/protobuf-architect/SKILL.md). This skill covers only the generated Connect surface.

- **Generate with `buf`** (`buf.build/connectrpc/go`), `paths=source_relative`, through `task generate` — never by hand. Config in [RECIPES.md](RECIPES.md). The generated `*connect.go` is never edited; write helpers in sibling hand-written files.
- **The generated interface is the port.** `protoc-gen-connect-go` emits `UserServiceHandler` (one method per RPC) plus `NewUserServiceHandler(svc, opts...) (string, http.Handler)`. Your `userServer` struct implements `UserServiceHandler`; the generated shim holds no business logic.
- **Mount the returned path on a `http.ServeMux`** — one `mux.Handle(path, handler)` per service, alongside non-RPC routes (health, reflection, CORS). Use the path `NewUserServiceHandler` returns; don't hand-write route strings.
- **Request/response are always typed messages.** Unary handlers take `*connect.Request[T]` and return `*connect.Response[T]`; streaming handlers take `*connect.ServerStream[T]`, `*connect.ClientStream[T]` or `*connect.BidiStream[Req, Resp]`.

## 3. Error handling — `connect.NewError`

Return errors via `connect.NewError(connect.Code, err)`. Map domain errors to codes in one central place (skeleton in [RECIPES.md](RECIPES.md)).

Full code table in [RECIPES.md](RECIPES.md#connect-code-reference).

- **Don't reach for `CodeInternal` as a default.** Map every known domain error to a specific code; `CodeUnknown` is even worse.
- **`connect.CodeOf(err)`** recovers the code from any error — use it in the logging interceptor and in tests, never invent your own `error → int` map.
- **`connect.NewErrorDetail`** attaches structured detail (`google.rpc.ErrorInfo`, `google.rpc.BadRequest`), read back with `connect.DetailsFromError` — the RPC analogue of REST's RFC 9457 (see [rest-api-architect §7](../rest-api-architect/SKILL.md)). Include a request id in the detail.
- **Never leak stack traces or DB errors** to clients. Log server-side; return a generic message plus the correlation id. The HTTP status mapping differs per protocol — gRPC-Web can't render every detail the way Connect's JSON body does, so if a detail matters to a browser, also put it in the message text.

## 4. Interceptors — mandatory chain

Interceptors are Connect's middleware, built with `connect.UnaryInterceptorFunc` and composed with `connect.ChainUnaryInterceptors`. **Order matters** — outermost first: **recovery → request-id → logging → auth → validation → metrics**. Full chain in [RECIPES.md](RECIPES.md).

- **Recovery first** — catches panics anywhere downstream and converts to `CodeInternal` with the correlation id (never a stack trace). Use named returns in the interceptor so the deferred recover can set both response and error.
- **Request-id second** — read `X-Request-Id` from `req.Header()` or mint one, inject it into `context.Context`, and echo it on the response header.
- **Auth before validation** — no point validating an unauthenticated request's body. Auth reads the bearer token, verifies it locally against cached JWKS, and stores the principal in the context; per-method authorization happens in the handler.
- **Validation is centralized** via [connectrpc.com/validate](STACK.md) (protovalidate, per [protobuf-architect §5](../../encoding/protobuf-architect/SKILL.md#5-validation--protovalidate-cel)) — `validate.NewInterceptor()`, no hand-written checks per handler.
- **Metrics/OTel last** — [connectrpc.com/otelconnect](STACK.md) instruments every call (latency, codes, RPC attributes) without touching handlers. Streams need their own chain (`connect.ChainStreamInterceptors`), but it runs for all three protocols; only the rendered error detail differs.

## 5. Streaming patterns

Connect supports the four gRPC call types through the same handler.

| Pattern | Use for | Pitfalls |
|---|---|---|
| **Unary** | Default — request/response | None — start here |
| **Server-stream** | Server emits N responses to one request (event feeds, tail, chunked export) | Connection state outlives the request; needs resume tokens |
| **Client-stream** | Client uploads N messages, server returns one summary (batch ingest) | Needs HTTP/2 on the wire |
| **Bidi-stream** | Genuinely interactive (chat, control protocols) | Needs HTTP/2; reconnect/resume logic; always bounded by a deadline |

- **The protocol gotcha:** Connect's own protocol carries unary and server-streaming over HTTP/1.1, but **client- and bidi-streaming require HTTP/2**. Browsers are server-stream only — gRPC-Web (what `@connectrpc/connect-web` speaks) has no client- or bidi-streams, so make browser uploads unary and only subscriptions stream.
- **Server-streams need resume tokens** — pass `start_after_id` or a cursor in the request so a disconnected client resumes from a known point rather than replaying from zero.
- **Don't stream to save round-trips** — unary with cursor pagination is usually simpler; streaming earns its place when the response is unbounded or genuinely incremental. **Always set a deadline on streams** — an unbounded stream is a leak.

## 6. Deadlines, metadata & context

Every call carries a deadline. Clients set it; servers respect it; downstream calls inherit the remaining time. Skeleton in [RECIPES.md](RECIPES.md).

- **Set deadlines with `context.WithTimeout`** on the client; the runtime propagates them as `grpc-timeout` metadata. Defaults: unary 5–30s; server-stream often minutes but always bounded.
- **Servers check `ctx.Err()`** in long handlers and abort the moment it fires; propagate `ctx` to DB and outbound calls.
- **Metadata is HTTP headers.** Read request metadata with `req.Header().Get(...)`; write response metadata with `resp.Header().Set(...)` and trailers with `resp.Trailer().Set(...)`. Standardize one correlation-id header (`X-Request-Id`) — Connect uses plain HTTP header casing, so don't rely on gRPC's lowercase normalization.

## 7. Health, reflection & ops

- **Health:** `connectrpc.com/grpchealth` implements `grpc.health.v1.Health` over all three protocols — `grpchealth.NewHandler(grpchealth.NewStaticChecker(serviceName))`. Always on; orchestrators and load balancers need it.
- **Reflection:** `connectrpc.com/grpcreflect` enables `grpcurl` and IDE introspection without the `.proto` — mount `NewHandlerV1` (and `NewHandlerV1Alpha` for older clients) from a `NewStaticReflector(...)`. **Dev only, off in prod**; it leaks the whole service surface.
- **CORS:** when browsers call the service directly, wrap the mux with `connectrpc.com/cors` (`cors.New(cors.WithAllowedOrigins(...), cors.AllowedMethods(), cors.AllowedHeaders(), cors.ExposedHeaders(), cors.MaxAge(...))`) — never `Allow-Origin: *` for authenticated endpoints (same rule as [rest-api-architect §10](../rest-api-architect/SKILL.md)). Opt into response compression with `connect.WithSendGzip()` / `connect.WithCompressMinBytes(...)`.

## 8. Client side

Generated clients are generic and typed — no reflection at call time.

- **`NewUserServiceClient(httpClient, baseURL, opts...)`** from the generated package, backed by `connect.NewClient[Req, Resp](...)` under the hood. Pass `*http.Client` (with sane timeouts, TLS config, and your tracing transport).
- **Force a protocol when needed:** `connect.WithGRPC()` or `connect.WithGRPCWeb()`; the default is Connect. `connect.WithProtoJSON()` selects JSON on the Connect protocol. Set a `context.WithTimeout` on every call, then `defer cancel()`.
- **Compose retries/backoff through the house `resilience` module** ([RESILIENCE](../../personal/go-house-libs/references/RESILIENCE.md)) rather than hand-rolling a loop around the client — it classifies permanent vs transient errors and adds jitter.
- **No per-request internal calls** — validate tokens locally against cached JWKS; don't call an auth service on the hot path. Client interceptors use the same `connect.UnaryInterceptorFunc` shape via `connect.WithInterceptors(...)`, so outbound calls reuse the request-id/tracing interceptors.

## 9. Testing & conformance

- **`httptest` for Connect.** Build the mux, wrap it in `httptest.NewServer`, and point a generated client at `srv.URL` using `srv.Client()`. Call the handler directly with `connect.NewRequest(...)` when you don't need the wire; use `httptest` when protocol behavior (headers, codes, streaming) matters. No `bufconn` — that is a gRPC-only convenience for testing the native adapter ([grpc-architect §9](../grpc-architect/SKILL.md)).
- **The house pattern is a conformance suite.** Define scenarios once (success, each error code, deadline exceeded, unauthenticated, not-found, pagination) and run each against three transports of one Go port: Connect over `httptest`, native gRPC over `bufconn`, and the REST surface if one exists. A single shared table with a per-transport client factory proves all surfaces map errors identically — see [RECIPES.md](RECIPES.md).
- **Table-driven tests** per [go-architect §9](../../languages/go-architect/SKILL.md#9-testing) — one row per (input, expected `connect.Code`, expected error type), asserted with `connect.CodeOf`. `grpcurl` remains the ad-hoc tool against a reflection-enabled dev server.

## 10. When to pick Connect vs vanilla gRPC vs REST

Comparison table in [RECIPES.md](RECIPES.md#connect-vs-vanilla-grpc-vs-rest-comparison).

- **Connect is the default for a new Go RPC service.** It costs nothing over vanilla gRPC when the caller is another backend, and removes the gateway when the caller is a browser.
- **Vanilla gRPC stays the right call** when every peer is pure gRPC and you want the leanest possible server, or when an existing gRPC ecosystem (interceptors, `bufconn` tooling) is already in place — both can share the same `.proto`.
- **REST is the right call for the public surface** — external consumers, cache-friendly reads, `OpenAPI` documentation.

Files in this skill

  • DIGEST.md2.2 KB
  • RECIPES.md9.5 KB
  • SKILL.md12.4 KB
  • STACK.md3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in development →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.