//! Example showing how to convert errors into responses and how one might do //! dependency injection using trait objects. //! //! Run with //! //! ```not_rust //! cargo run -p example-error-handling-and-dependency-injection //! ``` use axum::{ async_trait, extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, routing::{get, post}, Json, Router, }; use serde::{Deserialize, Serialize}; use serde_json::json; use std::sync::Arc; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use uuid::Uuid; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "example_error_handling_and_dependency_injection=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); // Inject a `UserRepo` into our handlers via a trait object. This could be // the live implementation or just a mock for testing. let user_repo = Arc::new(ExampleUserRepo) as DynUserRepo; // Build our application with some routes let app = Router::new() .route("/users/:id", get(users_show)) .route("/users", post(users_create)) .with_state(user_repo); // Run our application let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } /// Handler for `GET /users/:id`. /// /// Extracts the user repo from request extensions and calls it. `UserRepoError`s /// are automatically converted into `AppError` which implements `IntoResponse` /// so it can be returned from handlers directly. async fn users_show( Path(user_id): Path, State(user_repo): State, ) -> Result, AppError> { let user = user_repo.find(user_id).await?; Ok(user.into()) } /// Handler for `POST /users`. async fn users_create( State(user_repo): State, Json(params): Json, ) -> Result, AppError> { let user = user_repo.create(params).await?; Ok(user.into()) } /// Our app's top level error type. enum AppError { /// Something went wrong when calling the user repo. UserRepo(UserRepoError), } /// This makes it possible to use `?` to automatically convert a `UserRepoError` /// into an `AppError`. impl From for AppError { fn from(inner: UserRepoError) -> Self { AppError::UserRepo(inner) } } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_message) = match self { AppError::UserRepo(UserRepoError::NotFound) => { (StatusCode::NOT_FOUND, "User not found") } AppError::UserRepo(UserRepoError::InvalidUsername) => { (StatusCode::UNPROCESSABLE_ENTITY, "Invalid username") } }; let body = Json(json!({ "error": error_message, })); (status, body).into_response() } } /// Example implementation of `UserRepo`. struct ExampleUserRepo; #[async_trait] impl UserRepo for ExampleUserRepo { async fn find(&self, _user_id: Uuid) -> Result { unimplemented!() } async fn create(&self, _params: CreateUser) -> Result { unimplemented!() } } /// Type alias that makes it easier to extract `UserRepo` trait objects. type DynUserRepo = Arc; /// A trait that defines things a user repo might support. #[async_trait] trait UserRepo { /// Loop up a user by their id. async fn find(&self, user_id: Uuid) -> Result; /// Create a new user. async fn create(&self, params: CreateUser) -> Result; } #[derive(Debug, Serialize)] struct User { id: Uuid, username: String, } #[derive(Debug, Deserialize)] #[allow(dead_code)] struct CreateUser { username: String, } /// Errors that can happen when using the user repo. #[derive(Debug)] enum UserRepoError { #[allow(dead_code)] NotFound, #[allow(dead_code)] InvalidUsername, }