Declarative macro magic from Axum in Rust


From Scratch Code

It's like code, but from scratch.

Take a quick glance at the code snippet below. Without thinking too hard, is get a function or a method? How about post?

What did you decide: is get a function or a method? It appears to be both! And the same with post!

If you are familiar with API development in Rust, you may recognize this syntax from Axum. This puzzling question piqued my curiosity and led me to build Cairo, where I re-implemented some key Axum concepts in a simpler way as a learning exercise. I’ve long been fascinated by the intermediate + advanced concepts library authors employ to make their interfaces feel frictionless. Rust sits in a brilliant space because of the way it melds high-level expressiveness with low-level control and performance. One of the ways it accomplishes this is using macros.

What is a macro in Rust?

Macros in Rust come in two key forms: declarative and procedural. This post will focus on declarative, but let’s briefly define both.

A declarative macro in Rust uses the function!(..) syntax, which you may be familiar with from print!(“Hello World!”) or panic!(“at the disco!”). It is a form of metaprogramming which allows developers to reduce boilerplate code. It does this by evaluating the declarative macro at compile time, which produces more Rust code which ultimately ends up in the binary of the program.

If you have wondered why print!(..) in Rust is a macro, it is due to its dynamic nature. All of these are valid Rust:

A procedural macro in Rust uses the #[...] syntax, which you may have seen in #[tokio:main]. This macro is applied to a Rust function or struct, allowing the macro to modify the syntax tree of the annotated item at compile time. While it often helps reduce boilerplate code, it does so by transforming the function or struct as a whole rather than directly inserting statements into the function body. For example, #[tokio::main] wraps the entire main function in a tokio async runtime, enabling it to block on the execution of async code in main.

Code without macros can be repetitive and say the same thing in multiple ways

Axum is one of the most popular web frameworks in Rust. Its compatibility with the Tokio ecosystem and its powerful syntax, among other features, keep it near the front of the pack.

Skipping back to my original bafflement at whether get is a function or a method, the answer is both and it does this using a declarative macro.

If we look at our snippet of interest, get must be a function available in the outer scope and a method on whatever type is returned by invoking post. Similarly, post must be a function available in the outer scope and a method on whatever type is returned by invoking get. This symmetry pleases me. Let’s write some code that accomplishes exactly that and nothing more.

At this point, we see some duplication, but the boilerplate isn’t overwhelmingly negative. However, what about when we add support for the remaining HTTP verbs: OPTIONS, HEAD, PUT, DELETE, PATCH? We’d find ourselves maintaining 7 functions and 7 methods. Given the choice between maintaining 14 blocks of code versus adding complexity to prove I know a language, I’ll choose the latter every single time. Enter declarative macros.

Want to build your own HTTP server from scratch in Rust?

Declarative Macros in Axum

The code below is a mix of Axum and Cairo. We introduce two macros: add_http_function and add_http_method.

You’ll notice that these both accept two ident parameters, a $name and a $method. The $name becomes the function name, which will be get, post, etc., while the $method is concatenated to Method::, meaning we must give a valid Method enum variant. It’s okay if this is confusing at first—metaprogramming is a different way of thinking about programming. Instead of asking “what should my code do?” we are now asking “what code should my code produce?”

You’ll also notice that while they both define a “function” with the name $name, add_http_method accepts a parameter self. This means we must invoke our macro (by calling add_http_method!(get, Get)) in a context which is aware of a self. For us, this will be inside our impl InnerType block.

Putting it all together

Putting it all together, here is our new library with minimal boilerplate and support for method chaining for 7 HTTP methods.

The End

While we’ve only scratched the surface of the metapossibilities of Rust’s declarative macros, I hope this post has inspired you to explore more of what Rust can do at compile time. Procedural macros are another wonderful beast which I would encourage you to dig into if you are interested in ASTs and language development.

If you are curious about more ways Axum and APIs work under-the-hood, I encourage you to check out my course From Scratch: HTTP Server in Rust. The final module is public as the repo Cairo, which shows the north star you will build towards throughout the course. In addition to the macro usage we described here, you’ll explore the details of HTTP and how to create a Router which accepts handlers with variable parameters and return types using advanced type erasure.

Now it’s your turn! I’d love your perspective on these two questions:

1. How have you used declarative macros in your own Rust projects to reduce boilerplate code?

2. Are there any Rust crates you have used with an interfaces that makes you go “how did they do that?!”

Click here to view this post in your browser, copy the code snippets, and leave a comment.

Elsewhere

In addition to mentoring software engineers, I also write about my experience as an adult-diagnosed autistic person. Less code and the same number of jokes.

Why do I crave recognition?

The tension between ambition, connection, and individual needs.

From Scratch Code
Master how programming languages work under the hood by building projects from scratch. Offering tutoring, mentorship, and courses, in a supportive and fun environment.

🔗 Visit our Blog | ✉️ Contact Us

From Scratch Enterprises LLC
418 Broadway, Ste N, Albany, NY 12207
Unsubscribe · Preferences

From Scratch Code

Unlock the power of Rust and Python by mastering advanced concepts to build code that demonstrates your expertise and creativity. Offering mentorship and courses to help you create complex libraries, systems, and real-world tools from scratch—all in a supportive and sometimes silly environment.

Read more from From Scratch Code

From Scratch Code Helping you master Rust and Python, from scratch. A few months into development, I decided my north star for Memphis would be to run a Flask server entirely within my interpreter. I had no idea how much work this would entail, only that it sounded cool and would probably teach me a lot along the way. If I were making this goal today, I may pick FastAPI or nothing at all because that was silly of me. Python stdlib A big decision I encountered was how to deal with the Python...

From Scratch Code Helping you master Rust and Python, from scratch. I’m currently exploring two interesting topics for Memphis, my Python interpreter in Rust: building for WebAssembly and embedding CPython. With no major milestones to report this week, I thought I’d share some in-progress thoughts. For me, Memphis is been a project for expanding my conceptual understanding through practical experiments—hopefully, this post can do the same for you as we walk through some of the design...

From Scratch Code Helping you master Rust and Python, from scratch. THE BIG CITY—From Scratch Enterprises LLC (ticker: FSEL) announced its newest venture Monday, From Scratch Code (ticker: FSC). Members of the media gathered around the folding chair of its owlish founder, Jones Beach. Refreshments were not provided. Whispers circulated among the media contingent that this was the same desk which produced the not-a-non-profit, From Scratch dot org (ticker: FSdo). The representative present...