Overview
gRPC is arguably Rust's strongest protocol suit: tonic (server + client over
hyper/h2) with prost (protobuf codegen) is a first-class stack, and — the
differentiating fact — a genuine
tower citizen: a tonic server
is a tower Service, so the auth/tracing/limit middleware you wrote for axum applies
to your gRPC endpoints unchanged. Where
grpc-java is the reference implementation and
grpc-go the flagship, tonic is a community stack that became
the obvious default — including inside major infrastructure (linkerd's proxy among
others).
Key points
- Codegen at build time:
tonic-buildinbuild.rs(ortonic_buildvia buf) compiles.protointo prost structs (plain Rust with#[derive(Message)]) and tonic service traits — no reflection, no runtime descriptors unless you opt in. - Implementing a service = implementing a trait:
#[tonic::async_trait] impl Greeter for MyGreeterwithRequest<T>/Response<T>— the schema-first contract as a compiler obligation, the strongest form of the pattern across the three ecosystems. - All four streaming modes map to
Streams: unary, server-streaming (type ListStream = Pin<Box<dyn Stream<...>>>, or tokio_stream wrappers), client-streaming (Streaming<T>argument), bidirectional. Backpressure comes from h2 flow control + the async pull model, no extra API. - tower is the middleware story: interceptors for the light auth/metadata cases,
full tower
Layers for everything else — plus tonic-specific layers from the ecosystem (tonic-tracing, tonic middleware for metrics). - The operational endpoints ship as crates:
tonic-health(grpc.health.v1 for load balancers),tonic-reflection(server reflection for grpcurl/Postman),tonic-web(gRPC-Web for browsers, as a layer). - buf over raw protoc: the same schema-governance conclusions as the neighbours —
buf generatewith the prost/tonic plugins,buf breakingin CI; proto files are the shared polyglot contract, Rust is just another consumer. - prost design choices to know:
optionalmaps toOption<T>, unknown enum values decode to a sentinel (i32repr),bytescan map toBytesfor zero-copy; well-known types via prost-types. No runtime reflection meansgrpcurl-style dynamic clients need the reflection service turned on.
Examples
// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.bytes(["."])
.compile_protos(&["proto/orders.proto"], &["proto"])?;
Ok(())
}
#[tonic::async_trait]
impl Orders for OrderSvc {
async fn get_order(&self, req: Request<GetOrderRequest>)
-> Result<Response<Order>, Status> {
let order = self.store.find(req.into_inner().id).await
.map_err(|_| Status::not_found("no such order"))?;
Ok(Response::new(order))
}
}
Server::builder()
.layer(TraceLayer::new_for_grpc()) // the same tower-http layer family
.add_service(OrdersServer::new(svc))
.add_service(health_service)
.serve(addr).await?;
Related
- Backend protocols in Rust — the map — parent map.
- Frameworks × tower/hyper — why one middleware stack covers REST and gRPC here.
- Messaging in Rust — the asynchronous alternative for service-to-service work.
- gRPC in Java and gRPC in Go — the reference implementations; tonic matches them feature-for-feature on the server/client core.