01. Project Setup
📋 Jump to Takeaways🎁 A Rust web service in 2026 starts with three crates and one command. Less ceremony than you might expect.
Creating the Project
cargo new url-shortener
cd url-shortenerThat creates src/main.rs and Cargo.toml. Everything starts here.
Adding Dependencies
Edit Cargo.toml:
[package]
name = "url-shortener"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"- axum — the web framework built on tokio and tower
- tokio — the async runtime;
features = ["full"]enables everything - serde + serde_json — JSON serialization
Run cargo build to download and compile dependencies. First build takes a minute — subsequent builds are incremental and fast.
Project Structure
Start flat. Don't create ten modules on day one:
url-shortener/
├── Cargo.toml
├── Cargo.lock ← commit this — it pins exact dependency versions
└── src/
└── main.rsWe'll grow the structure as the project needs it. The rule: add a module when a file gets too long, not before.
The Entry Point
main.rs is the wiring layer. It starts the runtime, builds the router, and binds to a port. No business logic here:
use axum::{Router, routing::get};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/health", get(health));
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
async fn health() -> &'static str {
"ok"
}#[tokio::main] starts the Tokio runtime and makes main async. axum::serve runs the server and blocks until it's stopped.
Run it:
cargo runcurl http://localhost:3000/health
# okCargo.lock
Always commit Cargo.lock for applications (binaries). It pins the exact versions of every dependency so builds are reproducible. For libraries you publish to crates.io, don't commit it — consumers use their own resolution.
Key Takeaways
cargo newcreates the project;Cargo.tomldeclares dependenciesaxum,tokio, andserdeare the three crates every Rust web service starts with#[tokio::main]starts the async runtimeaxum::servetakes aTcpListenerand aRouterand runs the server- Start flat — add modules when files get too long, not before
- Commit
Cargo.lockfor binaries, not for libraries
🎁 A health endpoint returns a string. But real routes return JSON, accept path parameters, and read request bodies. How does Axum handle all of that without a macro in sight?