Structuring a Rust gRPC Service with Separate Module for Protobuf-Generated Code

Project Structure

This guide demonstrates how to organize a Rust gRPC server by separating protobuf-generated code into its own module.

project/
├── build.rs                # Protobuf code generation
├── Cargo.toml              # Rust project configuration
├── proto/
│   └── euclidolap.proto    # Protocol buffer definitions
├── src/
│   ├── euclidolap/
│   │   ├── mod.rs          # Module entry point
│   │   └── euclidolap.rs   # Generated gRPC code
│   └── main.rs             # Application entry point

build.rs Configuration

Configure the build script to output generated code to the src/euclidolap/ directory:

fn main() {
    let generated_dir = "src/euclidolap";

    if let Err(e) = tonic_build::configure()
        .out_dir(generated_dir)
        .compile(&["proto/euclidolap.proto"], &["proto"])
    {
        eprintln!("Proto compilation failed: {}", e);
        std::process::exit(1);
    }

    println!("cargo:rerun-if-changed=proto/euclidolap.proto");
}

The out_dir setting directs the generated Rust files into the specified module directory.

Module Entry Point

Create src/euclidolap/mod.rs to expoce the generated code:

pub mod euclidolap {
    include!("euclidolap/euclidolap.rs");
}

The include! macro brings the generated code into scope within the module.

Service Implementation

The main application file implements the gRPC service logic:

mod euclidolap;

use euclidolap::euclidolap::olap_service_server::{OLAPService, OLAPServiceServer};
use euclidolap::euclidolap::{OLAPRequest, OLAPResponse, Row};
use tonic::{transport::Server, Request, Response, Status};

#[derive(Debug, Default)]
pub struct AnalysisService {}

#[tonic::async_trait]
impl OLAPService for AnalysisService {
    async fn execute_operation(
        &self,
        request: Request<OLAPRequest>,
    ) -> Result<Response<OLAPResponse>, Status> {
        let payload = request.into_inner();
        println!("Request received - Type: {}, Query: {}", 
                 payload.operation_type, 
                 payload.statement);

        let result = OLAPResponse {
            rows: vec![Row {
                columns: vec!["Data_A".to_string(), "Data_B".to_string()],
            }],
        };

        Ok(Response::new(result))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listen_addr = "0.0.0.0:50052".parse().unwrap();
    let service_instance = AnalysisService::default();

    println!(">>> Analysis Server running on {} <<<", listen_addr);

    Server::builder()
        .add_service(OLAPServiceServer::new(service_instance))
        .serve(listen_addr)
        .await?;

    Ok(())
}

The AnalysisService struct implements the OLAPService trait, handling incoming requests and returning mock responses.

Protocol Buffer Definition

The corresponding .proto file:

syntax = "proto3";

package euclidolap;

service OLAPService {
  rpc ExecuteOperation(OLAPRequest) returns (OLAPResponse);
}

message OLAPRequest {
  string operation_type = 1;
  string statement = 2;
}

message OLAPResponse {
  repeated Row rows = 1;
}

message Row {
  repeated string columns = 1;
}

Generated Artifacts

Running cargo build produces src/euclidolap/euclidolap.rs containing:

  • Rust structs mapping to each protobuf message
  • Trait definitions for service implementation (e.g., OLAPServiceServer)
  • Serialization and deserialization implementations

Execution Flow

  1. Build Phase: cargo build triggers build.rs, generating Rust code from euclidolap.proto
  2. Server Startup: The application binds to 0.0.0.0:50052 and registers the service
  3. Request Handling: Incoming OLAPRequest messages are processed by execute_operation
  4. Response Delivery: The method returns an OLAPResponse containing resultt rows

Tags: rust gRPC Protobuf tonic code-generation

Posted on Wed, 03 Jun 2026 16:29:20 +0000 by jonsimmonds