The most surprising thing about gRPC services in Rust with Tonic is how much of the boilerplate you don’t have to write, even as you gain fine-grained control over the underlying transport and serialization.

Let’s see it in action. We’ll define a simple "Greeter" service with a SayHello RPC.

First, the .proto file. This is the contract.

syntax = "proto3";

package greeter;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Next, we need to generate Rust code from this. Tonic integrates with prost and tonic-build. In your build.rs file:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::configure()
        .compile(
            &["proto/greeter.proto"], // Path to your .proto file
            &["proto/"],              // Include directory
        )?;
    Ok(())
}

This build.rs script will produce Rust modules containing your service and message definitions. When you run cargo build, this code gets generated.

Now, the server implementation. We’ll use the generated code.

use tonic::{transport::Server, Request, Response, Status};
use greeter::greeter_server::{Greeter, GreeterServer};
use greeter::{HelloReply, HelloRequest};

// The generated code provides this module for us.
pub mod greeter {
    tonic::include_proto!("greeter"); // The string here matches the package name in .proto
}

#[derive(Debug, Default)]
pub struct MyGreeter;

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloReply>, Status> {
        println!("Incoming request: {:?}", request);

        let reply = HelloReply {
            message: format!("Hello {}!", request.into_inner().name),
        };
        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:50051".parse()?;
    let greeter_service = MyGreeter::default();

    println!("Greeter server listening on {}", addr);

    Server::builder()
        .add_service(GreeterServer::new(greeter_service))
        .serve(addr)
        .await?;

    Ok(())
}

The tonic::include_proto!("greeter"); macro is key here. It pulls in the code generated by prost based on your .proto file. The #[tonic::async_trait] is necessary because gRPC handlers are async. The Server::builder() is how you configure the gRPC server, adding your implemented service.

On the client side, it’s similarly straightforward.

use greeter::greeter_client::GreeterClient;
use greeter::HelloRequest;

// Use the same generated module
pub mod greeter {
    tonic::include_proto!("greeter");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GreeterClient::connect("http://127.0.0.1:50051").await?;
    let request = tonic::Request::new(HelloRequest {
        name: "Tonic".into(),
    });

    let response = client.say_hello(request).await?;

    println!("RESPONSE={:?}", response.into_inner().message);
    Ok(())
}

Here, GreeterClient is also generated code. You connect to the server’s address and then call the RPC method directly. Tonic handles the HTTP/2 framing, Protobuf serialization/deserialization, and error mapping for you.

The mental model is that your .proto file defines a set of Rust traits and structs. You implement the traits on your own structs (like MyGreeter), and Tonic’s runtime uses these implementations to serve requests. On the client, Tonic generates a convenient client struct that can call these methods, abstracting away the network details.

The way Tonic handles Request and Response internally is worth noting. When you receive a request, Request<T> wraps your message T. This wrapper can carry additional metadata like headers. Similarly, Response<T> allows you to send back metadata along with your message. This is how you’d pass things like authentication tokens or tracing IDs across the wire. You don’t see the raw HTTP/2 frames or Protobuf bytes because Tonic manages that layer.

The next logical step is to explore how to handle streaming RPCs (client-side, server-side, and bidirectional) within Tonic.

Want structured learning?

Take the full Grpc course →