InFeeo
Language

A Rust macros use case: Tightly-coupled API definitions for a client and server(x.com)

×
Link preview Aden (@aaaaaaaaden) auf X I'm a software engineer in San Francisco, currently working on a platform to streamline release management for VPCs: Bottlerocket. Posts 2026-06-23 A Rust macros use case: Tightly-coupled API definitions for the client and server A Rust macros use case: Tightly-coupled API definitions for the client and server I’m working on a Kubernetes operator and an API server for it to interface with. Both of these are crates in the same Rust workspace. The motivation for this was that I wanted to define all of the types used by the two services in one place, and keep the services tightly-coupled. When writing the operator, I wrote a protocol to define the HTTP requests the operator sends to the server: pub trait ApiPath { type Request: serde::Serialize; type Response: serde::de::DeserializeOwned; const METHOD: Method; const PATH: &'static str; } So that I could define paths/endpoints like this: pub async fn send(&self, body: P::Request) -> Result { let url = format!("https://{}/{}", self.host, P::PATH); self.client .request(P::METHOD, url) .bearer_auth(&self.token) .json(&body) .send() .await? .error_for_status()? .json::() .await } This worked super well when implementing the operator. All I had to do was call this generic send function with a struct that implemented ApiPath, which reduced a lot of boilerplate code, and let me define the method, body type, response type and path all in one place for each endpoint. When I started implementing the API server, which I used the axum crate for, I was having a hard time adding the routing/handling in an elegant way using the protocol I created for the operator. When defining a route in axum, you usually do something like this: axum::Router::new().route("/poll", axum::routing::get(poll)); The issue here though is that I’m now defining the path and method for each endpoint in a different place. I could do something like this, but I don’t really like how I’m specifying the endpoint struct in two places: axum::Router::new().route(Poll::PATH, method_to_handler_func(Poll::METHOD)(handler)); I was only vaguely familiar with macros and thought I’d see if this would be a good use case. After some iteration I got this: macro_rules! into_route { ($path:ty, $handler:expr) => { axum::Router::new().route(::PATH, from_method(::METHOD, $handler)) }; } * from_method just matches the http::Method to the axum handler, e.g. axum::routing::get. Now I can just define my router like this: let operator_routes = axum::Router::new() .merge(into_route!(Poll, poll)) .merge(into_route!(ListImages, list_images)) All I have to do is pass in the struct implementing the ApiPath protocol and the handler function! This was my first practical use of macros, and I thought it was interesting enough to share. Source: https://x.com/aaaaaaaaden X (formerly Twitter) · x.com
I'm a software engineer in San Francisco, currently working on a
platform to streamline release management for VPCs:
Bottlerocket.

Posts

2026-06-23
A Rust macros use case: Tightly-coupled API definitions for the
client and server

A Rust macros use case: Tightly-coupled API definitions for the
client and server

I’m working on a Kubernetes operator and an API server for it to
interface with. Both of these are crates in the same Rust workspace.
The motivation for this was that I wanted to define all of the types
used by the two services in one place, and keep the services
tightly-coupled. When writing the operator, I wrote a protocol to
define the HTTP requests the operator sends to the server:

pub trait ApiPath {
type Request: serde::Serialize;
type Response: serde::de::DeserializeOwned;

const METHOD: Method;
const PATH: &'static str;
}

So that I could define paths/endpoints like this:

pub async fn send(&self, body: P::Request) -> Result {
let url = format!("https://{}/{}", self.host, P::PATH);

self.client
.request(P::METHOD, url)
.bearer_auth(&self.token)
.json(&body)
.send()
.await?
.error_for_status()?
.json::()
.await
}

This worked super well when implementing the operator. All I had to do
was call this generic send function with a struct that implemented
ApiPath, which reduced a lot of boilerplate code, and let
me define the method, body type, response type and path all in one place for each
endpoint.

When I started implementing the API server, which I used the
axum crate for, I was having a hard time adding the
routing/handling in an elegant way using the protocol I created for the
operator. When defining a route in axum, you usually do something like
this:

axum::Router::new().route("/poll", axum::routing::get(poll));

The issue here though is that I’m now defining the path and method for
each endpoint in a different place. I could do something like this, but
I don’t really like how I’m specifying the endpoint struct in two
places:

axum::Router::new().route(Poll::PATH, method_to_handler_func(Poll::METHOD)(handler));

I was only vaguely familiar with macros and thought I’d see if this
would be a good use case. After some iteration I got this:

macro_rules! into_route {
($path:ty, $handler:expr) => {
axum::Router::new().route(::PATH, from_method(::METHOD, $handler))
};
}

* from_method just matches the
http::Method to the axum handler, e.g.
axum::routing::get.

Now I can just define my router like this:

let operator_routes = axum::Router::new()
.merge(into_route!(Poll, poll))
.merge(into_route!(ListImages, list_images))

All I have to do is pass in the struct implementing the
ApiPath protocol and the handler function! This was my first practical use of macros, and I thought it was interesting enough to share.

Log in Log in to comment.

No comments yet.