Course contentsShow
← Rust
Lesson 765 of 1,9106. Generics and Static DispatchFree lesson

Generic RPC Client Design

Define generic service endpoints that automate the serialization of request payloads.

In this lesson, you will learn how to design a generic RPC client that leverages Rust’s trait system to automatically serialize arbitrary request payloads into a unified network format.

The Problem: Boilerplate Requests

When building an RPC client, you often end up writing manual serialization code for every method. If you have a GetUser request and a PostMessage request, you don't want to reinvent the logic for converting those structs into bytes, sending them over the wire, and deserializing the response. We want a single, generic client that handles the "plumbing" while letting us define the "payloads" separately.

Defining the Endpoint Trait

To achieve this, we define a trait that acts as a contract for any data structure we want to send over the network. This trait forces each request to specify its own response type:

pub trait RpcRequest {
    type Response;
    fn path(&self) -> &str;
}

By using an associated type (type Response), we ensure that the compiler knows exactly what to expect back from the server as soon as we define a request struct.

Building the Generic Client

Now, our client doesn't need to know about GetUser or PostMessage. It only needs to know how to talk to things that implement RpcRequest. Because we are using static dispatch, the compiler generates a specialized version of the call method for every specific request type, ensuring zero runtime overhead:

struct RpcClient { /* network transport fields */ }

impl RpcClient {
    pub fn call<R: RpcRequest + Serialize>(&self, request: R) -> R::Response 
    where R::Response: DeserializeOwned 
    {
        let payload = serde_json::to_vec(&request).unwrap();
        let raw_response = self.transport.send(request.path(), payload);
        serde_json::from_slice(&raw_response).unwrap()
    }
}

Why This Works

By passing a generic R into the call method, the RpcClient treats the request as a black box. The logic for how to serialize the data is delegated to serde (via the Serialize trait bound), while the RpcRequest trait provides the necessary metadata (like the URL path). This keeps your client code clean, type-safe, and highly reusable for any new RPC endpoints you add later.

Key Takeaway: By using traits to define the relationship between a request payload and its response type, you can build a generic client that handles serialization and transport logic for any number of endpoints without writing redundant code.