Skip to content

leozw/connect-dual-protocol-example

Repository files navigation

connect-dual-protocol-example

One Go binary. One port. Three protocols (Connect, gRPC, gRPC-Web). A canonical, didactic template for any new Elven Works / Sentinel v2 microservice that needs RPC reachable from browsers, mobile clients, and other Go services.


1. What this is

This repository is a minimum-yet-complete reference showing how to expose one RPC service over the Connect, gRPC, and gRPC-Web protocols from a single Go binary on a single port. It is intentionally small: every file teaches one idea. The whole project is readable in an afternoon. Copy its structure into a new service and replace the domain.

It is not a production microservice — no business domain, no persistence, no multi-tenant auth, no Dockerfile, no Kubernetes. The point is the wiring: where the protocols plug in, where interceptors fit, how the toolchain produces code from the proto.

2. Why Connect (not grpc-go + grpc-gateway)?

A traditional Go gRPC service that needs to be reachable from browsers normally runs two stacks: grpc-go for service-to-service traffic and grpc-gateway to expose REST/JSON over HTTP/1.1. Two code paths, two routers, two surfaces that drift apart.

Connect collapses that to one stack. A connectrpc.com/connect handler natively serves:

  • the Connect protocol (own framing, JSON or binary, HTTP/1.1 or HTTP/2),
  • gRPC (binary, HTTP/2 only),
  • gRPC-Web (binary, HTTP/1.1 or HTTP/2).

…off the same http.Handler. One mux, one port, three wire formats. No proxy, no duplicated code. See https://connectrpc.com for the project's philosophy.

3. The three protocols side-by-side

Protocol Transport Codec When to use
Connect HTTP/1.1 or HTTP/2 JSON or binary Browser, curl, debugging, edge — anything that wants plain HTTP/JSON.
gRPC HTTP/2 only binary Internal service-to-service traffic where binary framing wins.
gRPC-Web HTTP/1.1 or HTTP/2 binary Browser code talking to a gRPC backend without a translation proxy.

Caveat: gRPC-Web does not support client-streaming or full bidi streaming (HTTP/1.1 limitation). Use Connect or gRPC for those — see the integration test for proof.

4. Quick start

# 1. Install pinned codegen plugins and the linter (one-time, idempotent).
make tools

# 2. Resolve BSR dependencies and regenerate Go code from proto.
make deps
make gen

# 3. Tests, including the matrix integration test.
make test

# 4. In one terminal:
make run-server

# 5. In another terminal:
make demo       # nine-step end-to-end demo across all three protocols

Run a single CLI call:

EXAMPLE_AUTH_TOKEN=dev-token-please-change-me \
    go run ./cmd/client connect greet --name=Leo --lang=pt-BR

5. Anatomy — what each file teaches

proto/elven/example/v1/greeter.proto
    The schema. One service, four RPCs, protovalidate annotations.
    Conforms to buf's STANDARD lint preset — every RPC owns its request and response type.

buf.yaml                Buf v2 workspace: module, lint, breaking, BSR deps.
buf.gen.yaml            Code-gen: managed mode + local plugins, output to gen/go.
buf.lock                BSR digests (committed; regenerate with `make deps`).

cmd/server/main.go      The server. Reads top to bottom: config → logger → otel
                        → newHandler → http.Server with H2C → graceful shutdown.
cmd/server/integration_test.go
                        The proof — every RPC × every protocol against an httptest.Server.

cmd/client/main.go      The star of the show. One `client.Greet(...)` call site,
                        three transports. Read its header comment first.

internal/greeter/       The business logic. Four RPC handlers + a salutation table.
internal/interceptors/  Recovery, logging, and the authn HTTP middleware. README inside
                        explains the chain order and the middleware-vs-interceptor split.
internal/otel/          Minimal OTel bootstrap: OTLP/gRPC exporters with noop fallback.

scripts/demo.sh         The nine-step demo run by `make demo`.
.github/workflows/ci.yaml
                        Buf lint+breaking and Go lint+test+build, all SHA-pinned.

6. The four RPC kinds

RPC kind Wire shape Example
Unary one request → one response client connect greet --name=Leo
Server-stream one request → many responses client connect stream --name=Leo --count=5
Client-stream many requests → one response client connect batch --names=Leo,Hugo,Ana
Bidi-stream many in, many out, interleaved client connect chat --names=Leo,Hugo,Ana

The generated Go interfaces give each kind a distinct handler signature — *connect.ServerStream[T], *connect.ClientStream[T], *connect.BidiStream[Req,Res], etc. See internal/greeter/greeter.go for the matching method bodies, and cmd/client/main.go for the matching client-side patterns.

7. Interceptors and middleware — read this

This is the most important conceptual section. Connect distinguishes two wrap layers:

HTTP request
  └─ authn.Middleware                              (transport-level auth — HTTP middleware)
       └─ http.ServeMux                            (dispatch by path)
            └─ NewGreeterServiceHandler(svc,
                 connect.WithInterceptors(
                   recovery,                       (outermost RPC interceptor)
                   logging,
                   otelconnect,
                   validate,                       (innermost — closest to handler)
                 ))
                 └─ greeter.Service.Greet/Stream/Batch/Chat

The rationale for the order — and crucially, why authn is HTTP middleware rather than a connect.Interceptor — is laid out in internal/interceptors/README.md.

8. How to copy this into a new service

  1. Clone (or use as a GitHub template repo).
  2. Rename the Go module:
    go mod edit -module github.com/your-org/your-service
    Update imports in cmd/, internal/, and the go_package_prefix in buf.gen.yaml.
  3. Replace proto/elven/example/v1/greeter.proto with your real schema. Keep one request/response message pair per RPC to stay STANDARD-lint clean.
  4. Replace internal/greeter with your real domain logic. Keep the interceptor and OTel wiring as-is — they are domain-independent.
  5. Replace the static bearer token in internal/interceptors/auth.go with real auth (OIDC, mTLS, an identity service). The middleware shape stays the same; only the AuthFunc changes.
  6. Extend the server config in cmd/server/main.go for any extra env vars your service needs; the helpers (envOr, envDurationOr) are easy to copy.
  7. Run make gen test demo to confirm the wiring still holds together.

9. What is intentionally NOT here

Missing Why
Dockerfile / Helm / deploy Belongs in your real service repo. Adding it here would dilute the wiring focus.
Persistence (Postgres, Redis) The greeter is in-memory on purpose. Plug your real store into internal/greeter (or a new package next to it).
Multi-tenant / RBAC authn is intentionally minimal so you can see the seam between transport-auth and RPC.
OIDC, mTLS, real identity Static bearer token is a stand-in — swap the AuthFunc for real validation when you copy this.
Metrics dashboards, SLO docs Belong in your real service. The OTel scaffolding here makes them straightforward to add later.

10. References

Releases

No releases published

Packages

 
 
 

Contributors