I'm working on a personal project where I'd like to auto-discover API routes based on macros.
What I'm Trying to Achieve
Code Example - Defining Routes:
// Simple health check
#[get("/health")]
async fn health() -> &'static str {
"OK"
}
// JSON response
#[get("/")]
async fn index() -> Json<serde_json::Value> {
Json(serde_json::json!({
"message": "Welcome to Azap!",
"version": "0.1.0"
}))
}
Expected Generated Code:
let app = Router::new()
.route("/health", axum::routing::get(health))
.route("/", axum::routing::get(index))
.route("/users/:id", axum::routing::get(get_user))
.route("/users", axum::routing::get(list_users))
.route("/users", axum::routing::post(create_user));
Current Implementation
I've implemented attribute macros like this:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, LitStr};
#[proc_macro_attribute]
pub fn get(attr: TokenStream, input: TokenStream) -> TokenStream {
route_macro("get", attr, input)
}
#[proc_macro_attribute]
pub fn post(attr: TokenStream, input: TokenStream) -> TokenStream {
route_macro("post", attr, input)
}
#[proc_macro_attribute]
pub fn put(attr: TokenStream, input: TokenStream) -> TokenStream {
route_macro("put", attr, input)
}
#[proc_macro_attribute]
pub fn patch(attr: TokenStream, input: TokenStream) -> TokenStream {
route_macro("patch", attr, input)
}
#[proc_macro_attribute]
pub fn delete(attr: TokenStream, input: TokenStream) -> TokenStream {
route_macro("delete", attr, input)
}
fn route_macro(method: &str, attr: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
let path = parse_macro_input!(attr as LitStr);
let fn_name = &input_fn.sig.ident;
let fn_vis = &input_fn.vis;
let fn_sig = &input_fn.sig;
let fn_block = &input_fn.block;
let fn_attrs = &input_fn.attrs;
let method_upper = method.to_uppercase();
let metadata_const = quote::format_ident!(
"__AZAP_ROUTE_{}_{}",
method_upper,
fn_name.to_string().to_uppercase()
);
let expand = quote! {
#(#fn_attrs)*
#fn_vis #fn_sig {
#fn_block
}
#[doc(hidden)]
#[allow(non_upper_case_globals)]
pub const #metadata_const: azap::RouteMetaData = azap::RouteMetaData {
method: #method,
path: #path,
handler_name: stringify!(#fn_name),
module: module_path!(),
file: file!()
};
};
TokenStream::from(expand)
}
The Challenge: Route Registration
Now I'm stuck on the route registration/discovery phase.
Solutions I've Tried (That Didn't Work)
1. Using linkme to store all RouteMetaData
The idea was to collect all routes at build time using linkme's distributed slice feature.
Problems with this approach:
- Axum's
Routerneeds function pointers that aren't available yet in the metadata - linkme runs at the linking stage, which happens after macro expansion:
Because of this ordering, the linkme slice is always empty when we try to access it during macro expansion.build script → macro expansion → function expansion → linkme
2. Hybrid approach: build script + macros
Problems:
- Build scripts run before macro expansion, so the macros haven't generated the metadata yet
- No clear way to collect routes that are defined via macros
What I'm Looking For
I'd like a better solution for auto-discovering routes defined across multiple files and modules. My expected directory structure looks like this:
routes/
├── health.rs → /health
├── auth/
│ ├── mod.rs
│ ├── login.rs → /auth/login
│ └── register.rs → /auth/register
└── users/
├── mod.rs
├── get.rs → /users, /users/:id
└── create.rs → /users
I want route discovery based on:
- File paths
- HTTP methods
- Routes defined at the function level
And then automatically register them with the Axum Router.
Questions:
- Is there a way to collect metadata from attribute macros at compile time?
- Should I be using a different approach entirely (e.g., a derive macro on a root module)?
- Has anyone solved a similar problem for Axum or other web frameworks?
Any guidance would be greatly appreciated!