gRPC & Connect
go.putnami.dev/grpc provides a gRPC server plugin and a Connect protocol gateway for serving gRPC over HTTP/1.1.
Build-time service inventory
The describe pass emits schema/grpc-services.json with the services registered
by native gRPC servers, Connect gateways and API bridges. Native registrations
include method names and streaming flags, using an unstarted server without
binding a listener. Gateways report their mounted prefix and unknown methods;
bridges report the exact POST methods they bind. Reflection services appear
only when reflection is enabled.
Compose these plugins with Use so describe sees the configured application.
One writer emits the complete union, including when describe targets one
surface. A configured surface with no services emits an empty list. An absent
file leaves the facts unavailable. A successful build removes stale inventory
when its producer is removed; a failed describe preserves the committed copy.
See the service inventory contract for the canonical protocol and the route joins for each transport.
gRPC server plugin
Basic setup
import (
"go.putnami.dev/app"
"go.putnami.dev/grpc"
)
plugin := grpc.NewPlugin(grpc.Config{
Port: 9090, // env: GRPC_PORT
})
// Register gRPC services
plugin.Register(func(s *grpc.Server) {
pb.RegisterUserServiceServer(s, &userServer{})
pb.RegisterOrderServiceServer(s, &orderServer{})
})
a := app.New("my-service")
a.Module.Use(plugin)
a.ListenAndServe()Configuration
| Field | Type | Default | Env var | Description |
|---|---|---|---|---|
Port |
int |
9090 |
GRPC_PORT |
gRPC server port |
Server options
plugin := grpc.NewPlugin(grpc.Config{Port: 9090})
// Add interceptors
plugin.WithUnaryInterceptor(myInterceptor)
plugin.WithStreamInterceptor(myStreamInterceptor)
// Enable server reflection (for grpcurl, etc.)
plugin.WithReflection(true)
// Pass raw grpc.ServerOption
plugin.WithServerOption(grpc.MaxRecvMsgSize(10 << 20))Built-in interceptors
Logging
Logs each RPC call with method, duration, and status:
import "go.putnami.dev/logger"
plugin.WithUnaryInterceptor(grpc.LoggingInterceptor(logger.Default()))Recovery
Catches panics in handlers and returns an Internal error:
plugin.WithUnaryInterceptor(grpc.RecoveryInterceptor(logger.Default()))DI scoping
Creates a DI scope per gRPC request, enabling per-request service resolution:
import "go.putnami.dev/inject"
plugin.WithUnaryInterceptor(grpc.DIInterceptor(containerContext))Connect protocol gateway
The Connect gateway exposes gRPC services over HTTP/1.1 with JSON encoding, making them accessible from browsers and REST clients.
Setup
import (
fhttp "go.putnami.dev/http"
"go.putnami.dev/grpc"
)
server := fhttp.NewServerPlugin(fhttp.ServerConfig{Port: 3000})
gateway := grpc.NewGatewayPlugin(grpc.GatewayConfig{})
// Mount Connect handlers
gateway.MountHandler("/users.v1.UserService/", userConnectHandler)
// Register on the HTTP server
gateway.RegisterOn(server)
a := app.New("my-service")
a.Module.Use(server)
a.Module.Use(gateway)Calling Connect endpoints
Connect endpoints are accessible as standard HTTP POST requests:
POST /users.v1.UserService/GetUser
Content-Type: application/json
{"id": "user-123"}Response:
{"name": "Jane", "email": "jane@example.com"}Lifecycle
Both the gRPC plugin and Connect gateway implement the standard lifecycle interfaces:
- Configure — register services, configure interceptors
- Start — begin listening for connections
- Stop — graceful shutdown with drain
Application.Start()
→ gRPC.Configure() — register services
→ gRPC.Start() — listen on :9090
→ Gateway.Configure() — mount handlers
→ Gateway.Start() — ready for HTTP traffic
Application.Stop()
→ gRPC.Stop() — graceful stopProtobuf contracts
go.putnami.dev/proto renders a .proto service definition from the same
api.Plugin route metadata the OpenAPI document is generated from:
apiPlugin := api.New(httpServer)
protoPlugin := proto.NewPlugin(proto.PluginOptions{
PackageName: "myapp.v1",
GoPackage: "example.com/myapp/proto/v1",
}).From(apiPlugin)
a.Use(httpServer).Use(apiPlugin).Use(protoPlugin)putnami build writes the document to schema/api.proto. The plugin writes
nothing while the service is starting or running; to export it outside a Putnami
build, call protoPlugin.WriteTo("") after the configure phase.
RPC names are derived from method and path (GET /users → ListUsers,
POST /users → CreateUsers), and the api → Connect bridge derives its URLs the
same way, so a generated client and the served route agree.
Bridging api endpoints to Connect
grpc.NewApiBridge exposes every bound api.Endpoint at
POST /<package>.<Service>/<RPCName> by wrapping the endpoint's own handler, so
validation, security, and error shape are identical to a direct REST call.
The request body is a namespaced envelope, which keeps identically named fields from colliding:
{ "params": { "id": "42" }, "query": { "expand": "true" }, "body": { "id": "renamed" } }The bridge carries JSON only — Protobuf binary encoding and streaming methods are
outside its contract — and endpoints finalized with Document() are skipped,
since their handlers are mounted elsewhere.
Related guides
- HTTP & Middleware — HTTP server for Connect gateway
- Plugins & Lifecycle — plugin lifecycle
- Dependency Injection — per-request DI scoping
Support and contract
go.putnami.dev/grpc and go.putnami.dev/proto are stable in the workspace
support catalog. gRPC behavior is defined by the gRPC services specification and
two accepted decision records under go/framework/grpc/; the Protobuf document
is one half of the API contracts specification under go/framework/api/specs/,
with its own decision record under go/framework/proto/doc/adr/. Before v1.0.0 a
minor 0.x release may still contain a documented breaking change; strict
compatibility between every pre-1.0 minor is not promised.