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:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
axumaxum 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 lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
Modernize how you debug your Rust apps — start monitoring for free.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
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 now