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.
What you’ll build
Section titled “What you’ll build”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.
The stack, and why each piece is here
Section titled “The stack, and why each piece is here”| Crate | Role | Why it earns its place |
|---|---|---|
| tokio | async runtime | drives thousands of concurrent requests on a handful of OS threads |
| axum | web framework | routing + typed extractors that turn requests into your function arguments |
| sqlx | async SQL | real queries to SQLite, async all the way down, no ORM magic |
| serde | (de)serialization | JSON in and out, derived from your structs |
| tracing | structured logs | one span per request, with method, path, status, and latency |
| thiserror | error types | one 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 requestThe four days
Section titled “The four days”| Day | Chapter | You add | The Rust it forces |
|---|---|---|---|
| 8 | Async & axum | a running server, routes, JSON | async/.await, futures are lazy, Send across .await |
| 9 | State & sqlx | a DB pool, shared state, CRUD | State, Clone-cheap Arc, runtime queries, FromRow |
| 10 | Errors & middleware | AppError, tracing, config | IntoResponse, ?→HTTP, From, layered middleware |
| 11 | Tests & Docker | integration tests, a Dockerfile | in-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.
The thread
Section titled “The thread”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 →