typestar

tour_api.rs in Rust

A whole small service: shared state, nested routes, typed errors, JSON replies.

use std::collections::BTreeMap;
use std::sync::Arc;

use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;

#[derive(Clone)]
struct AppState {
    tours: Arc<RwLock<BTreeMap<String, Tour>>>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Tour {
    slug: String,
    title: String,
    steps: u32,
}

#[derive(Deserialize)]
struct NewTour {
    slug: String,
    title: String,
    #[serde(default)]
    steps: u32,
}

enum ApiError {
    NotFound(String),
    Duplicate(String),
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ApiError::NotFound(s) => {
                (StatusCode::NOT_FOUND, format!("no tour {s}"))
            }
            ApiError::Duplicate(s) => {
                (StatusCode::CONFLICT, format!("{s} exists"))
            }
        };
        (status, Json(serde_json::json!({"error": message}))).into_response()
    }
}

async fn list(State(state): State<AppState>) -> Json<Vec<Tour>> {
    let tours = state.tours.read().await;
    Json(tours.values().cloned().collect())
}

async fn show(
    Path(slug): Path<String>,
    State(state): State<AppState>,
) -> Result<Json<Tour>, ApiError> {
    let tours = state.tours.read().await;
    tours
        .get(&slug)
        .cloned()
        .map(Json)
        .ok_or(ApiError::NotFound(slug))
}

async fn create(
    State(state): State<AppState>,
    Json(input): Json<NewTour>,
) -> Result<(StatusCode, Json<Tour>), ApiError> {
    let mut tours = state.tours.write().await;
    if tours.contains_key(&input.slug) {
        return Err(ApiError::Duplicate(input.slug));
    }
    let slug = input.slug.clone();
    let tour = Tour { slug, title: input.title, steps: input.steps };
    tours.insert(input.slug, tour.clone());
    Ok((StatusCode::CREATED, Json(tour)))
}

#[tokio::main]
async fn main() {
    let state = AppState { tours: Arc::new(RwLock::new(BTreeMap::new())) };

    let app = Router::new()
        .route("/healthz", get(|| async { "ok" }))
        .nest(
            "/api",
            Router::new()
                .route("/tours", get(list).post(create))
                .route("/tours/{slug}", get(show)),
        )
        .with_state(state);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

How it works

  1. The state holds an in-memory tour table behind an RwLock.
  2. Handlers borrow it through State and answer with Json.
  3. The error enum maps every failure onto a status code.

Keywords and builtins used here

The run, in numbers

Lines
100
Characters to type
2184
Tokens
666
Three-star pace
115 tpm

At the three-star pace of 115 tokens a minute, this run takes about 347 seconds.

Type this snippet

Step 1 of 1 in Encore, step 9 of 9 in Web services with axum.

← Previous