Updated Aug 27, 2026

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 code
  • GET /links — list all links with pagination
  • DELETE /links/:code — delete a link (authenticated)
  • GET /:code — redirect to the original URL (increments click count)
  • POST /auth/login — exchange credentials for a JWT
  • GET /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 queries

main.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:

  • WebSocketsaxum::extract::WebSocketUpgrade for real-time features
  • Background jobstokio::spawn for async tasks, or apalis for job queues
  • Cachingredis crate with connection pooling via deadpool-redis
  • OpenAPIutoipa generates API documentation from your types
  • Observabilityopentelemetry for 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 AppError type with IntoResponse gives consistent error responses across the entire API
  • AuthUser as an extractor means any handler can require auth just by declaring it
  • Graceful shutdown is two lines: .with_graceful_shutdown(signal) and pool.close()
  • Multi-stage Docker builds produce tiny images; environment variables handle all config

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy