15. Put It All Together
📋 Jump to Takeaways🎁 Fifteen lessons of Rust web services. Here's the complete picture — every piece connected, every decision justified.
What We Built
A URL shortener API with:
POST /links— create a short code for a URL (authenticated)GET /links/:code— get a link by codeGET /links— list all links with paginationDELETE /links/:code— delete a link (authenticated)GET /:code— redirect to the original URL (increments click count)POST /auth/login— exchange credentials for a JWTGET /health— liveness check with database ping
Final Project Structure
url-shortener/
├── Cargo.toml
├── Cargo.lock
├── .env
├── .env.example
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── migrations/
│ └── 20260101000000_create_links_table.sql
└── src/
├── main.rs ← wires everything together
├── config.rs ← Config::from_env()
├── state.rs ← AppState
├── error.rs ← AppError + IntoResponse
├── auth.rs ← JWT + AuthUser extractor
├── db.rs ← pool creation + migrations
└── links/
├── mod.rs ← router() function
├── handler.rs ← handler functions
├── model.rs ← Link struct
└── store.rs ← database queriesmain.rs — The Wiring
mod config;
mod state;
mod error;
mod auth;
mod db;
mod links;
use axum::Router;
use tokio::net::TcpListener;
use tower_http::trace::TraceLayer;
#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
init_tracing();
let config = config::Config::from_env().unwrap_or_else(|e| {
eprintln!("config error: {}", e);
std::process::exit(1);
});
let pool = db::create_pool(&config.database_url).await;
db::run_migrations(&pool).await;
let state = state::AppState { pool: pool.clone(), config };
let app = Router::new()
.merge(links::router(state.clone()))
.layer(TraceLayer::new_for_http());
let listener = TcpListener::bind(format!("0.0.0.0:{}", state.config.port))
.await
.unwrap();
tracing::info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
pool.close().await;
tracing::info!("shutdown complete");
}How the Concepts Connect
Ownership (Rust Essentials 05): AppState is cloned into each handler. PgPool clones cheaply because it's Arc-wrapped internally. No data is copied — just reference counts.
Traits (Rust Essentials 09): IntoResponse on AppError, FromRequest on AuthUser, FromRow on Link — the whole framework is built on traits you can implement for your own types.
Error Handling (Rust Essentials 07, 11): Every handler returns Result<T, AppError>. The ? operator propagates errors up. AppError::into_response converts them to HTTP responses at the boundary.
Async/Await (Rust Essentials 19): Every handler, every database query, every middleware is async. tokio::select! in shutdown, .await everywhere. The whole service runs on Tokio's thread pool.
Arc/Mutex (Rust Essentials 18): PgPool is Arc<...> under the hood. AppState derives Clone and shares the pool across all handlers safely.
Generics + Lifetimes (Rust Essentials 10, 12): FromRequest<S> and FromRequestParts<S> are generic over the state type. The compiler checks that your extractors work with your state.
What's Next
This course covered the fundamentals. The ecosystem goes further:
- WebSockets —
axum::extract::WebSocketUpgradefor real-time features - Background jobs —
tokio::spawnfor async tasks, orapalisfor job queues - Caching —
rediscrate with connection pooling viadeadpool-redis - OpenAPI —
utoipagenerates API documentation from your types - Observability —
opentelemetryfor distributed tracing across services
The patterns you've learned here — state injection, extractors, tower layers, async query functions — are the same patterns you'll use in all of them.
Key Takeaways
- A production Rust web service is: Axum + SQLx + Serde + tower-http + dotenvy + thiserror
- Handlers are plain async functions; extractors and state do the heavy lifting
- One
AppErrortype withIntoResponsegives consistent error responses across the entire API AuthUseras an extractor means any handler can require auth just by declaring it- Graceful shutdown is two lines:
.with_graceful_shutdown(signal)andpool.close() - Multi-stage Docker builds produce tiny images; environment variables handle all config