grpc-slides/docs/practical_grpc.md
2025-10-29 17:32:03 +01:00

11 KiB

A practical guide to gRPC with

rust logo

Victor Martinez


Contents

  • Ingredients to build a gRPC API
  • Defining our API with Protocol Buffers
    • Defining a request
    • Defining a response
    • About backwards compatibility
    • Defining a service
  • Generate a Rust library
    • Manual project setup
    • Build script
    • Exposing a library
  • CI & Release flow
    • The Buf tool
    • Checking for breaking changes
    • How to release
  • Implementing a gRPC service
    • Implementing the service trait
    • Parsing requests
    • Error handling
  • Building a gRPC server
    • Authentication
    • Tracing
    • Running our server
  • How to deploy
  • How to call our server
    • Creating a gRPC client
    • Authentication
    • Response handling
  • How is it going so far
    • Users
    • Metrics

Ingredients to build a gRPC API

protocol buffers logo Buf logo
Tonic logo

Defining our API

protocol buffers logo

Project structure

project structure

https://github.com/primait/es-policy-grpc


Project structure

project structure

https://github.com/primait/es-policy-grpc


Defining a request

syntax = "proto3";

package es_policy_grpc.messages.issue_policy.request.v1;

import "es_policy_grpc/domain/v1/bundle.proto";
import "es_policy_grpc/domain/v1/coverage.proto";
import "es_policy_grpc/domain/v1/issuing_company.proto";
// ... etc

message IssuePolicyRequest {
  google.protobuf.Timestamp requested_at = 1; 
  google.protobuf.Timestamp start_at = 2;
  google.protobuf.Timestamp end_at = 3;
  google.protobuf.Timestamp purchased_at = 4;
  es_policy_grpc.domain.v1.TransactionInformation transaction = 5;
  es_policy_grpc.domain.v1.IssuingCompany issuing_company = 6;
  string quote_id = 7;
  es_policy_grpc.domain.v1.QuoteSource quote_source = 8;
  string application_id = 9;
  string offer_id = 10;
  es_policy_grpc.domain.v1.Price price = 11;
  es_policy_grpc.domain.v1.Bundle bundle = 12;
  repeated es_policy_grpc.domain.v1.ProductCover covers = 13;
  es_policy_grpc.domain.v1.PolicyHolderInformation policy_holder_information = 14;
  es_policy_grpc.domain.v1.VehicleInformation vehicle_information = 15;
  es_policy_grpc.domain.v1.QuoteVersion quote_version = 16;
} 

Defining a request

syntax = "proto3";

package es_policy_grpc.domain.v1;

import "es_policy_grpc/domain/v1/price.proto";
import "google/protobuf/timestamp.proto";

message ProductCover {
  CoverType cover_type = 1;
  google.protobuf.Timestamp start_at = 2;
  google.protobuf.Timestamp end_at = 3;
  es_policy_grpc.domain.v1.Price price = 4;
  optional RoadsideAssistanceTier roadside_assistance_tier = 5;
}

enum CoverType {
  COVER_TYPE_UNSPECIFIED = 0;
  COVER_TYPE_MANDATORY_THIRD_PARTY_LIABILITY = 1;
  COVER_TYPE_VOLUNTARY_THIRD_PARTY_LIABILITY = 2;
  COVER_TYPE_DRIVER_ACCIDENT = 3;
  COVER_TYPE_WINDSHIELD = 4;
  COVER_TYPE_THEFT = 5;
  // ..etc
}

enum RoadsideAssistanceTier {
  ROADSIDE_ASSISTANCE_TIER_UNSPECIFIED = 0;
  ROADSIDE_ASSISTANCE_TIER_BASE = 1;
  ROADSIDE_ASSISTANCE_TIER_PREMIUM = 2;
  ROADSIDE_ASSISTANCE_TIER_PREMIUM_V2 = 3;
}

Defining a response

syntax = "proto3";

package es_policy_grpc.messages.issue_policy.response.v1;

message IssuePolicyResponse {
  string policy_id = 1;
}

About backwards compatibility


Required fields don't exist, everything has a default value

  • Strings => The empty string.
  • Bytes => Empty bytes.
  • Bools => False.
  • Numeric => Zero.
  • Message => Not set. Its exact value is language-dependent.
  • Enums => The first defined enum value, which must be 0.

https://protobuf.dev/programming-guides/proto3/#default


Enums must have an "Unspecified" variant

enum BundleSlug {
  BUNDLE_SLUG_UNSPECIFIED = 0;
  BUNDLE_SLUG_TERCEROS_BASICO = 1;
  BUNDLE_SLUG_TERCEROS_AMPLIADO = 2;
  BUNDLE_SLUG_TODO_RIESGO_CON_FRANQUICIA = 3;
  BUNDLE_SLUG_TODO_RIESGO_CON_FRANQUICIA_300 = 4;
  BUNDLE_SLUG_TODO_RIESGO_CON_FRANQUICIA_500 = 5;
} 

https://protobuf.dev/best-practices/dos-donts/#unspecified-enum


Field tags must never be reused

And deleted fields must be marked as reserved

message AmendWithdrawalRequest {
  string policy_id = 1;
  google.protobuf.Timestamp requested_at = 2;

  reserved 3;
  reserved "interruption_at";

  optional string description = 4;
  oneof reason {
    es_policy_grpc.messages.withdraw_policy.request.v1.CustomerWithdrawReason customer = 5;
  }
}

https://protobuf.dev/best-practices/dos-donts#reuse-number


Learn more about Do's and Dont's

https://protobuf.dev/best-practices/dos-donts


Defining a service

syntax = "proto3";

package es_policy_grpc.service.v1;

import "es_policy_grpc/messages/issue_policy/request/v1/request.proto";
import "es_policy_grpc/messages/issue_policy/response/v1/response.proto";

service PolicyManagementService {
  rpc IssuePolicy(es_policy_grpc.messages.issue_policy.request.v1.IssuePolicyRequest) returns (es_policy_grpc.messages.issue_policy.response.v1.IssuePolicyResponse);
} 

Building a library

Tonic logo

Project structure

project structure

Build script

use std::io::Result;

fn main() -> Result<()> {
    // List of proto files containing a message definition
    let proto_files = &[
        //Domain
        "proto/es_policy_grpc/domain/v1/address.proto",
        "proto/es_policy_grpc/domain/v1/bundle.proto",
        // etc.
        // Messages
        "proto/es_policy_grpc/messages/issue_policy/request/v1/request.proto",
        "proto/es_policy_grpc/messages/issue_policy/response/v1/response.proto",
        // Services
        "proto/es_policy_grpc/service/v1/service.proto",
    ];

    // Name of the folder containing the proto definitions
    let proto_folder = "proto";

    tonic_prost_build::configure()
        .protoc_arg("--experimental_allow_proto3_optional")
        .compile_protos(proto_files, &[proto_folder])
        .unwrap();

    Ok(())
}

Exposing the generated code

// src/lib.rs

pub mod domain {
    pub mod v1 {
        include!(concat!(env!("OUT_DIR"), "/es_policy_grpc.domain.v1.rs",));
    }
}

pub mod messages {
    pub mod issue_policy {
        pub mod request {
            pub mod v1 {
              include!(concat!(env!("OUT_DIR"), "/es_policy_grpc.messages.issue_policy.request.v1.rs"));
            }
        }

        pub mod response {
            pub mod v1 {
                include!(concat!(env!("OUT_DIR"), "/es_policy_grpc.messages.issue_policy.response.v1.rs"));
            }
        }
    }
}

pub mod policy_service {
    pub mod v1 {
        include!(concat!(env!("OUT_DIR"), "/es_policy_grpc.service.v1.rs"));
    }
} 

CI & Release flow


Buf CLI

  • A linter for proto files
  • A formatter for proto files
  • A system to organize your proto files by workspaces
  • A feature to check for breaking changes in your definitions
  • A plugin system to compile proto files into multiple formats
  • Editor integration
  • And more!

https://buf.build/product/cli

note:

  • Builds on top of protoc

  • Provides a very easy to use plugin and build system


CI workflows: Proto checks

name: "Check proto"

on:
  pull_request:
    paths:
      - "proto/**"

jobs:
  check-proto:
    # ..
    steps:
        # ..
      - name: Protobuf format check
        shell: bash
        run: |
          buf format -d --exit-code          

      - name: Protobuf lint
        shell: bash
        run: |
          buf lint          

CI workflows: Rust checks


name: "Check rust lib"

on:
  pull_request:
    paths:
      - "es-policy-grpc-rust/**"
      - "proto/**"
      - ".github/workflows/check-rust-lib.yml"

jobs:
  check-rust-lib-compiles:
    # ..
    container:
      image: public.ecr.aws/primaassicurazioni/actions-builder:ubuntu-24.04-3
    defaults:
      run:
        working-directory: es-policy-grpc-rust
    steps:
      - uses: actions/checkout@v4
      - uses: primait/shared-github-actions/actions/install-rust@install-rust-v3
        # ..
      - name: Install protoc
        # ..
      - name: Cargo build
        run: cargo build

CI workflows: Backwards compatibility check


name: "Check backwards compatibility"

on: pull_request

jobs:
   check-backward-compatibility:
    # ..
    steps:
     - uses: actions/checkout@v4

     - name: Install buf
     # ..
     - name: Fetch master branch
       shell: bash
       run: |
         git fetch origin master:master         

     - name: Check backwards compatibility against master
       shell: bash
       run: |
         buf breaking --against ".git#branch=master"         

Release workflow

name: "Release Rust Crate"

on:
  push:
    tags:
      - "*"
   
jobs:
  publish:
    # ..
    steps:
      - uses: actions/checkout@v4
      - uses: primait/shared-github-actions/actions/install-rust@rust-ci-v3
      # ..
      - name: Install cargo edit
      # ..
      - name: Set release version
        working-directory: es-policy-grpc-rust
        run: |
          cargo set-version $GITHUB_REF_NAME          
      - name: Retrieve vault token
        # ..
      - name: Install protoc compiler
      # ..
      - name: Publish package
        working-directory: es-policy-grpc-rust
        run: |
          cargo publish --allow-dirty          
        env:
          CARGO_REGISTRIES_ARTIFACTORY_TOKEN: ${{ format('Bearer {0}', CARGO_REGISTRIES_ARTIFACTORY_TOKEN) }}

Release workflow: Cargo configuration

# .cargo/config.toml

# Makes artifactory the default registry and saves passing --registry parameter
[registry]
default = "artifactory"
global-credential-providers = ["cargo:token"]

[registries]
artifactory = { index = "sparse+https://prima.jfrog.io/artifactory/api/cargo/policy-management-crates/index/" }
# Cargo.toml

[package]
name = "es-policy-grpc"
version = "0.6.7"
edition = "2024"
# ..
publish = ["artifactory"]