← Week 1: Protocol Buffers & gRPC

Day 4: tonic Server

Phase 3 · Jul 4, 2026

← Week 1: Protocol Buffers & gRPC

Agenda (2–3 hours)

  • Read (45 min): tonic getting-started guide; tonic server examples on GitHub
  • Study (45 min): How does tonic wrap a Service<Request<Body>> into a gRPC server? Trace the request flow from TCP accept to service method call
  • Practice (45 min): Implement a TaskService with all four RPC types; add server reflection; test with grpcurl
  • Challenge (30 min): Add request interceptor middleware that logs every RPC name and duration; attach request ID to the tonic context
← Week 1: Protocol Buffers & gRPC

Implementing a tonic Service

// src/service.rs
use tonic::{Request, Response, Status};
use myapp::v1::task_service_server::TaskService;
use myapp::v1::{GetTaskRequest, Task, WatchTaskRequest, TaskEvent};

#[derive(Debug, Default)]
pub struct TaskServiceImpl {
    tasks: Arc<RwLock<HashMap<String, Task>>>,
}

#[tonic::async_trait]
impl TaskService for TaskServiceImpl {
    async fn get_task(
        &self,
        request: Request<GetTaskRequest>,
    ) -> Result<Response<Task>, Status> {
        let tasks = self.tasks.read().await;
        let task = tasks.get(&request.into_inner().id)
            .ok_or_else(|| Status::not_found("task not found"))?;
        Ok(Response::new(task.clone()))
    }
    // ...
}
← Week 1: Protocol Buffers & gRPC

Server Streaming

type WatchTaskStream = ReceiverStream<Result<TaskEvent, Status>>;

async fn watch_task(
    &self,
    request: Request<WatchTaskRequest>,
) -> Result<Response<Self::WatchTaskStream>, Status> {
    let (tx, rx) = tokio::sync::mpsc::channel(16);
    let task_id = request.into_inner().task_id;

    tokio::spawn(async move {
        // Subscribe to task events
        loop {
            let event = next_event(&task_id).await;
            if tx.send(Ok(event)).await.is_err() {
                break; // client disconnected
            }
        }
    });

    Ok(Response::new(ReceiverStream::new(rx)))
}
← Week 1: Protocol Buffers & gRPC

Running the Server

use tonic::transport::Server;
use myapp::v1::task_service_server::TaskServiceServer;

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

    Server::builder()
        .add_service(TaskServiceServer::new(service))
        .add_service(tonic_reflection::server::Builder::configure()
            .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
            .build_v1()?)
        .serve(addr)
        .await?;
    Ok(())
}
← Week 1: Protocol Buffers & gRPC

Error Handling with Status

use tonic::{Status, Code};

// Standard status codes
Status::not_found("user not found")
Status::invalid_argument("id must be positive")
Status::internal("database error")
Status::unauthenticated("missing token")
Status::permission_denied("insufficient permissions")

// Rich error details (google.rpc.ErrorInfo)
use tonic_types::StatusExt;
let status = Status::with_error_details(
    Code::InvalidArgument,
    "invalid request",
    ErrorDetails::new().add_bad_request_violation("email", "invalid format"),
)?;
← Week 1: Protocol Buffers & gRPC

Key Takeaways

  • Implement the generated *Server trait; wrap with *ServiceServer::new() for tonic
  • Server-streaming returns a Stream<Item = Result<T, Status>> — use ReceiverStream
  • Tonic wraps Server::builder() → Tower middleware → service dispatch
  • Status maps to gRPC status codes; use tonic_types for rich error details

Tomorrow: tonic client — calling gRPC services with interceptors and metadata.