Axum Rest Example

Table of Contents

Purpose

Educational resource for coworkers looking to explore REST development with Rust. Quite opinionated.

The project itself provides an extremely simplified, API-only URL shortener with various sub-optimal choices and in no way represents a production-ready way to build such a service. It’s basically just enough to show how to wire up a database query to satisfy a request.

This document will generally not go into much project-agnostic detail about Rust practices and conventions.

Features

  • Read/write unauthenticated API endpoint to POST full URLs to and receive a shortened `hash` back
  • Configurable via TOML and/or environment variables
  • Can be run in a container via Docker Compose, along with a suite of observability tools around it
  • Task automation with Just
  • Database migrations via sqlx

Quickstart

This section still needs drastic improvement for audiences who are unfamiliar with any combination of Rust, Rustup, Nix, Cargo, etc.

Development

Non-Nix:

rustup toolchain install stable
cargo install --locked sqlx-cli
# i.e. postgresql://$USER:postgres@localhost/axum_rest_example_dev
export DATABASE_URL=...
sqlx -d $DATABASE_URL database create
sqlx -d $DATABASE_URL migrate run
cargo clippy --lib
cargo build
cargo test

Hot iteration loop using cargo-watch

cargo watch -x 'clippy --lib' -x 'test --lib' -x run

Nix with Direnv

Requires a recent Nix (2.7+) with Flakes support, plus nix-direnv.

direnv allow .

Nix without Direnv

Requires a recent Nix (2.7+) with Flakes support.

nix develop
export DATABASE_URL=...

MacOS, Non-Nix

Install Rustup, then:

cargo install --locked cargo-watch just sqlx-cli
export DATABASE_URL=...

Linux, Non-Nix

You’ll also need to install clang and lld for the faster linking experience.

Usage

Native, with just

# i.e. postgresql://$USER:postgres@localhost/axum_rest_example_dev
export DATABASE_URL=...
just migrate run

Native, no just

# i.e. postgresql://$USER:postgres@localhost/axum_rest_example_dev
export DATABASE_URL=...
sqlx -d $DATABASE_URL database create
sqlx -d $DATABASE_URL migrate run
cargo run

Client Examples

Using httpie:

http post :8080/v1/link destination=https://www.google.com/
HTTP/1.1 201 Created
content-length: 104
content-type: application/json
date: Fri, 10 Sep 2021 15:38:53 GMT

{
    "destination": "https://www.google.com/",
    "hash": "ghMW5",
    "id": "c92ead3b-f319-44e5-9764-6b12dffb5a46"
}
http get :8080/ghMW5
HTTP/1.1 307 Temporary Redirect
content-length: 0
date: Fri, 10 Sep 2021 15:39:18 GMT
location: https://www.google.com/

Goals

  • [ ] Demonstrate expressivity of Rust’s stdlib patterns such as Result/Option/enums/pattern-matching
  • [ ] Demonstrate utility of thiserror / anyhow for domain errors
  • [ ] Demonstrate utility of serde for handling structural issues with incoming payloads
  • [ ] Demonstrate viability of Rust for backend service development
  • [X] Don’t depend on beta releases of libraries to be able to compile with the latest Tokio
  • [X] Comprehensive use of async
  • [ ] Framework-level conventions and configurability
  • [ ] High-quality observability
  • [X] Don’t shy away from intermediate or advanced Rust if it’s needed
  • [X] Don’t shy away from community tooling that would be commonly used by an experienced practitioner

Included Components

Rust

Tokio

Axum

Tracing

Opentelemetry

Sqlx

Serde

Config

thiserror/anyhow

Non-Rust

Nix Flake

Direnv config with Nix Flake support

Dockerfile

  • Uses cargo-chef to produce cache-friendly layers containing just your dependencies

Docker-Compose environment

App

PostgreSQL

Grafana

Prometheus

Loki

Loki log driver config

Tempo

Documentation

  • This README
  • Rustdoc

To view local code-specific documentation, you may use:

cargo doc --features otel --document-private-items --open

Incomplete Planned Features

  • Prometheus-based technical and domain-specific metrics
  • Grafana dashboards and documentation
  • Documentation and automation improvements for guest contributors who are Nix-averse

Not Included

  • Non-trivial authentication
  • Any kind of ORM or database abstraction beyond sqlx

Decisions

Tokio over async-std

I find Tokio a technically more compelling suite of libraries, as well as finding its community extremely welcoming, curious, and active. async-std simply doesn’t seem to have the community or velocity I’m looking for, and I find a bifurcation of the Rust community along tribal lines to be extremely frustrating as a developer. You can’t use libraries written for one runtime in a project that uses another without wastefully running threads for both and needing translation layers anywhere they might need to directly interact. I’ve never once found an async-std -derived project compelling enough to look past the division it creates. I’m very confident that by comparison, fewer projects built with Tokio will have reason to regret their choice in 2-5 years.

Tracing over log/env_logger

I’ve found that many many projects and examples default to env_logger as an easy logging solution, but tracing is incredibly robust when you outgrow those features, and has been adopted by core community crates as well as part of the Rust toolchain itself.

Axum over Warp

Despite my background with Ruby’s Rack and Elixir’s Plug, I personally find Warp’s compiler messages to be quite opaque and struggled to be successful with it. Until axum became available, this was still the best choice to be on a relatively pure Tokio + Hyper stack.

Axum over Actix-Web

In order to build with a modern stable release of Tokio, you need to run beta versions of Actix-Web 4.0.x, and this has been the case for essentially all of 2021. They’re possibly getting close, but I more or less have stopped caring. Has pretty good docs and features, but also has a huge dependency footprint too. Finally, I’m not able to totally put all of the controversy around gaming the TechEmpower benchmarks behind me, which left a very bad taste in my mouth even as a bystander.

Axum over Rocket

Until very recently you flat out couldn’t use Rocket without nightly Rust, and if you stick to stable versions from Crates.io this continues to be the case. Also irregularly gives surprisingly poor results in comparative benchmarks, which I’m not willing to trade for decent ergonomics.

Sqlx over Diesel

sqlx is emphatically not an ORM, so you write raw SQL with occasional SQL-compatible annotations for type hinting at the boundary between Rust and SQL. Using its `query!` and `query_as!` family of macros, it allows compile-time checking of query semantics, i.e. have you written something that PostgreSQL understands to be a viable query given your current database schema? Uses pure SQL for DDL migrations.

Diesel appears to have a lot going for it ergnomically, but it is both not async and somewhere between apathetic and actively disinterested in being converted to async. That’s a total non-starter for me given that I am able to avoid such compromises anywhere else in the web development stack used for this project.