Skip to content

Project 3 · apilite — An Async REST API

In Project 2 · kvlite you built a key-value store and met Rust’s concurrency story head-on: traits, generics, lifetimes, and the borrow checker turning data races into compile errors. apilite is where that pays off. A web server is concurrency for a living — hundreds of requests in flight at once — and you’re about to build one whose hardest bugs the compiler simply refuses to let you write.

A small but real REST API for a notes resource, with full CRUD over HTTP and a SQLite database behind it:

GET /health → liveness probe
GET /notes → list all notes
POST /notes → create a note (201 Created)
GET /notes/{id} → fetch one note (404 if missing)
PUT /notes/{id} → replace a note's fields (404 if missing)
DELETE /notes/{id} → delete a note (204, or 404)

Every response is JSON. Every failure is a correct HTTP status code. The whole thing builds and tests with no live database and no extra tooling — the tests run against an in-memory SQLite, and a multi-stage Dockerfile ships it as a small image.

CrateRoleWhy it earns its place
tokioasync runtimedrives thousands of concurrent requests on a handful of OS threads
axumweb frameworkrouting + typed extractors that turn requests into your function arguments
sqlxasync SQLreal queries to SQLite, async all the way down, no ORM magic
serde(de)serializationJSON in and out, derived from your structs
tracingstructured logsone span per request, with method, path, status, and latency
thiserrorerror typesone AppError that maps cleanly to HTTP
┌─────────┐ HTTP ┌──────────────────────────────┐ SQL ┌────────┐
│ client │ ───────► │ axum router → handler │ ──────► │ SQLite │
│ (curl) │ JSON │ (extractors, AppState, ?) │ sqlx │ (file │
│ │ ◄─────── │ AppError → IntoResponse │ ◄────── │ / mem)│
└─────────┘ JSON └──────────────────────────────┘ rows └────────┘
▲ tokio runtime
└─ tracing layer wraps every request
DayChapterYou addThe Rust it forces
8Async & axuma running server, routes, JSONasync/.await, futures are lazy, Send across .await
9State & sqlxa DB pool, shared state, CRUDState, Clone-cheap Arc, runtime queries, FromRow
10Errors & middlewareAppError, tracing, configIntoResponse, ?→HTTP, From, layered middleware
11Tests & Dockerintegration tests, a Dockerfilein-process testing, a library crate, multi-stage builds

Each day teaches the concept at the moment the project needs it, then has you build that slice, and ends with Check your understanding (with answers). The companion crate lives at rust/apilite/cd rust/apilite && cargo run, or cargo test to watch the API exercise itself against an in-memory database.

What does building this force you to understand — and what is Rust’s compiler protecting you from? For an API the answer sharpens: a server shares data across many simultaneous tasks, and the same “shared XOR mutable” rule from The Rust Mindset now guards state shared across .await points and across tasks. You’ll see it as the Send/Sync bounds axum quietly requires — the compiler refusing to let you hand non-thread-safe data to a task that might resume on another thread. That refusal is a data race deleted before it could ship.

Start building: Day 8 · Async & axum →