axum
is an async web framework from the Tokio project. It is designed to be a very thin layer over hyper and is compatible with the Tower ecosystem, allowing the use of various middleware provided by tower-http
and tower-web
.
In this post, we will walk through how you can deploy a Rust web server to Heroku using axum
, Tokio, and GitHub Actions for your projects.
Jump ahead:
axum
axum
provides a user-friendly interface to mount routes on a server and pass handler functions.
axum
will handle listening to TCP sockets for connections and multiplexing HTTP requests to the correct handler and, as I mentioned, also allows the use of various middleware provided by the aforementioned Tower ecosystem.
use std::{net::SocketAddr, str::FromStr}; use axum::{ http::StatusCode, response::IntoResponse, routing::get, Router, Server, }; // running the top level future using tokio main #[tokio::main] async fn main() { // start the server run_server().await; } async fn run_server() { // Router is provided by Axum which allows mounting various routes and handlers. let app = Router::new() // `route` takes `/` and MethodRouter .route("/", // get function create a MethodRouter for a `/` path from the `hello_world` get(hello_world)) // create a socket address from the string address let addr = SocketAddr::from_str("0.0.0.0:8080").unwrap(); // start the server on the address // Server is a re-export from the hyper::Server Server::bind(&addr) // start handling the request using this service .serve(app.into_make_service()) // start polling the future .await .unwrap(); } // basic handler that responds with a static string // Handler function is an async function whose return type is anything that impl IntoResponse async fn hello_world() -> impl IntoResponse { // returning a tuple with HTTP status and the body (StatusCode::OK, "hello world!") }
Here, the Router
struct provides a route
method to add new routes and respective handlers. In the above example, get
is used to create a get handler for the /
route.
hello_world
is a handler which returns a tuple with the HTTP status and body. This tuple has an implementation for the IntoResponse
trait provided by axum
.
The Server
struct is a re-export of the hyper::Server
. As axum
attempts to be a very thin wrapper around hyper, you can expect it to provide performance comparable to hyper
.
The post
function is used to create a POST route on the provided path — as with the get
function, post
also takes a handler and returns MethodRoute
.
let app = Router::new() // `route` takes `/` and MethodRouter .route("/", // post function create a MethodRouter for a `/` path from the `hello_name` post(hello_name))
axum
provides JSON serializing and deserializing right out of the box. The Json
type implements both FromRequest
and IntoResponse
traits, allowing you to serialize responses and deserialize the request body.
// the input to our `hello_name` handler // Deserialize trait is required for deserialising bytes to the struct #[derive(Deserialize)] struct Request { name: String, } // the output to our `hello_name` handler // Serialize trait is required for serialising struct in bytes #[derive(Serialize)] struct Response{ greet:String }
The Request
struct implements the Deserialize
trait used by serde_json
to deserialize the request body, while the Response
struct implements the Serialize
trait to serialize the response.
async fn hello_name( // this argument tells axum to parse the request body // as JSON into a `Request` type Json(payload): Json<Request> ) -> impl IntoResponse { // insert your application logic here let user = Response { greet:format!("hello {}",payload.name) }; (StatusCode::CREATED, Json(user)) }
Json
is a type provided by axum
that internally implements the FromRequest
trait and uses the serde
and serde_json
crate to deserialize the JSON body in the request to the Request
struct.
Similar to the GET request handler, the POST handler can also return a tuple with the response status code and response body. Json
also implements the IntoResponse
trait, allowing it to convert the Response
struct into a JSON response.
Axum provides extractors as an abstraction to share state across your server and allows access of shared data to handlers.
// creating common state let app_state = Arc::new(Mutex::new(HashMap::<String,()>::new())); let app = Router::new() // `GET /` goes to `root` .route("/", get(root)) // `POST /users` goes to `create_user` .route("/hello", post(hello_name)) // Adding the state to the router. .layer(Extension(app_state));
Extension
wraps the shared state and is responsible for interacting with axum
. In the above example, the shared state is wrapped in Arc
and Mutex
to synchronize the access to the inner state.
async fn hello_name( Json(payload): Json<Request>, // This will extract out the shared state Extension(db):Extension<Arc<Mutex<HashMap<String,()>>>> ) -> impl IntoResponse { let user = Response { greet:format!("hello {}",payload.name) }; // we can use the shared state let mut s=db.lock().unwrap(); s.insert(payload.name.clone(), ()); (StatusCode::CREATED, Json(user)) }
Extension
also implements the FromRequest
trait that will be called by the axum
to extract the shared state from the request and pass it to the handler functions.
GitHub Actions can be used to test, build, and deploy Rust applications. In this section, we will focus on deploying and testing Rust applications.
# name of the workflow name: Rust # run workflow when the condition is met on: # run when code is pushed on the `main` branch push: branches: [ "main" ] # run when a pull request to the `main` branch pull_request: branches: [ "main" ] # env variables env: CARGO_TERM_COLOR: always # jobs jobs: # job name build: # os to run the job on support macOS and windows also runs-on: ubuntu-latest # steps for job steps: # this will get the code and set the git - uses: actions/checkout@v3 # run the build - name: Build # using cargo to build run: cargo build --release # for deployment - name: make dir # create a directory run: mkdir app # put the app in it - name: copy run: mv ./target/release/axum-deom ./app/axum # heroku deployment - uses: akhileshns/[email protected] with: # key from repository secrets heroku_api_key: ${{secrets.HEROKU_API_KEY}} # name of the Heroku app heroku_app_name: "axum-demo-try2" # email from which the app is uploaded heroku_email: "[email protected]" # app directory appdir: "./app" # start command procfile: "web: ./axum" # buildpack is like environment used to run the app buildpack: "https://github.com/ph3nx/heroku-binary-buildpack.git"
GitHub Actions provide support to stable versions of Rust by default. Cargo and rustc are installed by default on all supported operating systems by GitHub Actions — this is an action that run when the code is pushed to the main branch or when a pull request to the main branch is created.
on: # run when code is pushed on the `main` branch push: branches: [ "main" ] # run when a pull request to the `main` branch pull_request: branches: [ "main" ]
The workflow will first check the code, and then run the Cargo test to run the test on the code. It will then build the code using cargo-build.
The Cargo release will create a binary in the target folder, and the Action then copies the binary from the target folder to the ./app
folder for further use in the Heroku deployment step, which we will now proceed to.
Heroku doesn’t have an official buildpack for Rust, so there’s no official build environment for Rust apps with Heroku.
So instead, we will use GitHub Actions to build the app and deploy it to Heroku.
Heroku requires having a buildpack for each app, so binary-buildpack is used for Rust apps. There are community buildpacks for Rust, and since GitHub Actions are already being used to build the app, time can be saved by directly using the binary build on Heroku.
The GitHub Actions market has a very useful akhileshns/heroku-deploy
that deploys the Heroku app using GitHub Actions. In combination with binary-buildpack
, it becomes a powerful tool to deploy code.
- uses: akhileshns/[email protected] with: # key from repository secrets heroku_api_key: ${{secrets.HEROKU_API_KEY}} # name of the Heroku app heroku_app_name: "axum-demo-try2" # email from which the app is uploaded heroku_email: "[email protected]" # app directory appdir: "./app" # start command procfile: "web: ./axum" # buildpack is like environment used to run the app buildpack: "https://github.com/ph3nx/heroku-binary-buildpack.git"
To use this Action, a Heroku API key is needed. The key can be generated using the Heroku console in your account settings.
This action will create the app and deploy it for you. It takes the directory of the app and starts the command for the app, and you can also specify the buildpack you’d like to use.
Some code changes are required before the Rust app can be deployed to Heroku. Currently, the app uses an 8080
port, but Heroku will provide a different port for the app to use, so the Rust app should read the environment variable PORT
.
// read the port from env or use the port default port(8080) let port = std::env::var("PORT").unwrap_or(String::from("8080")); // convert the port to a socket address let addr = SocketAddr::from_str(&format!("0.0.0.0:{}", port)).unwrap(); // listen on the port Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap();
axum
is a very good web server framework with support for the wider tower-rs ecosystem. It allows the building of extensible and composable web services and offers performance benefits by offering a thin layer over hyper
.
GitHub Actions are great for CI/CD and allow for performing various automated tasks, such as building and testing code and generating docs on various platforms. GitHub Actions also support caching cargo dependencies to speed up Actions.
Heroku comes with support to autoscale continuous deployment, as well as support for hosted resources like databases and storage, for example. GitHub Actions and Heroku are independent of the framework, meaning the same action can test and deploy a web server written in Rocket or Actix Web — so feel free to experiment with whatever suits you!
When all of these tools are used together, they become a killer combo for developing and hosting Rust web servers. I hope you enjoyed following along with this tutorial — leave a comment about your experience below.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.