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
- The state holds an in-memory tour table behind an
RwLock. - Handlers borrow it through
Stateand answer withJson. - The error enum maps every failure onto a status code.
Keywords and builtins used here
ApiErrorAppStateArcErrJsonNewTourOkPathResponseResultStateStringTourVecasyncawaitcreateenumfnforifimplinputinto_responseletlistmainmatchmessagemutreturnselfshowstructu32use
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.
Step 1 of 1 in Encore, step 9 of 9 in Web services with axum.