Integrating gRPC
Why gRPC?
gRPC has gives us a nice way to declare our API in a schema definition and then code generate the server side implementation.
The data transfer protocol (Protobuf) is also useful if we want to do data storage i.e. for Big data projects. So we get 1 tool that can do 2 jobs.
Installation
Let's create a crate for our api definition and code generator.
$ cargo init --lib crates/grpc-api
Created library package
Install gRPC crates into your project cd
into your crates/grpc-api
folder.
Add the following to your app/Cargo.toml
below the [dependencies]
cargo add tonic
cargo add prost
cargo add serde --features derive
cargo add tonic-build --build
Create a folder called crates/grpc-api/protos
and a file called api.proto
.
├── .devcontainer/
│ └── ...
└── crates/
│ asset-pipeline/
│ └── ...
│ web-server/
│ └── ...
│ db/
│ └── ...
│ grpc-api/
│ ├── protos
│ │ └── api.proto
│ ├── src
│ │ └── lib.rs
│ ├── Cargo.toml
│ └── build.rs
│ ui-components/
│ └── ...
├── Cargo.toml
└── Cargo.lock
Defining the API
Add this to your api.proto
syntax = "proto3";
package api;
service Users {
rpc GetUsers(GetUsersRequest) returns (GetUsersResponse);
}
message GetUsersRequest {
}
message GetUsersResponse {
repeated User users = 1;
}
message User {
uint32 id = 1;
string email = 2;
}
Updating our build.rs
Add the following to your crates/grpc-api/build.rs
in the main
function.
use std::io::Result;
fn main() -> Result<()> {
tonic_build::configure()
.compile(
&["protos/api.proto"], &["protos"], )?;
Ok(())
}
Add a lib.rs
The crates/grpc-api/src/lib.rs
will load the code generated by tonic build and make it available to other crates.
pub mod api {
#![allow(clippy::large_enum_variant)]
#![allow(clippy::derive_partial_eq_without_eq)]
tonic::include_proto!("api");
}
Everything should compile at this point.
Implementing our API End Point
Create a file called crates/web-server/api_service.rs
and add the following implementation for our gRPC service.
use grpc_api::api::*;
use crate::errors::CustomError;
use db::queries;
use deadpool_postgres::Pool;
use tonic::{Request, Response, Status};
pub struct UsersService {
pub pool: Pool,
}
#[tonic::async_trait]
impl grpc_api::users_server::Fortunes for UsersService {
async fn get_users(
&self,
_request: Request<GetUsersRequest>,
) -> Result<Response<GetUsersResponse>, Status> {
let client = self
.pool
.get()
.await
.map_err(|e| CustomError::Database(e.to_string()))?;
let fortunes = queries::users::users(&client)
.await
.map_err(|e| CustomError::Database(e.to_string()))?;
let users = users
.into_iter()
.map(|user| User {
id: user.id as u32,
email: user.email,
})
.collect();
let users = GetUsersResponse {
users,
};
return Ok(Response::new(response));
}
}
Integrating Tonic and Axum
We can modify our Axum server so that it can respond to HTTP and gRPC requests.
Our crates/web-server/src/main.rs
now needs to look like this.
mod config;
mod errors;
use crate::errors::CustomError;
use axum::{extract::Extension, response::Json, routing::get, Router};
use std::net::SocketAddr;
use db::User;
use tower::{make::Shared, steer::Steer, BoxError, ServiceExt};
use tonic::transport::Server;
use grpc_api::api::api_server::UsersServer;
use http::{header::CONTENT_TYPE, Request};
#[tokio::main]
async fn main() {
let config = config::Config::new();
let pool = db::create_pool(&config.database_url);
let app = Router::new()
.route("/", get(users))
.layer(Extension(config))
.layer(Extension(pool.clone()))
.boxed_clone();
let grpc = Server::builder()
.add_service(TraceServer::new(api::trace_grpc_service::TraceService {
pool,
}))
.into_service()
.map_response(|r| r.map(axum::body::boxed))
.boxed_clone();
let http_grpc = Steer::new(vec![app, grpc], |req: &Request<Body>, _svcs: &[_]| {
if req.headers().get(CONTENT_TYPE).map(|v| v.as_bytes()) != Some(b"application/grpc") {
0
} else {
1
}
});
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service()).await.unwrap();
}
async fn users(Extension(pool): Extension<db::Pool>) -> Result<Json<Vec<User>>, CustomError> {
let client = pool.get().await?;
let users = db::queries::users::get_users()
.bind(&client)
.all()
.await?;
Ok(Json(users))
}
BloomRPC
To see our server working we can use BloomRPC with which we can load our api.proto
and fire off an RPC call to our fortunes API.
It will look something like the screenshot below.