axum-error-sets 0.2.0

Typed, composable HTTP error sets for Axum and Aide
Documentation

axum-error-sets

Crates.io Documentation License: MIT License: Apache 2.0

Typed, composable HTTP error sets for Axum and Aide.

Instead of monolithic error enums or loosely-typed responses, functions declare the exact set of HTTP status codes they can return using type-level tuple sets (e.g., (NotFound, Unauthorized)), powered by type-sets.


Features

  • No Monolithic Error Enums: Avoid constructing domain-wide error enums or per-function error types.
  • Exact Error Contracts: Functions declare precisely which HTTP status codes they can produce.
  • Subset-to-Superset Promotion: Error sets grow deterministically as they move up application layers via .into_superset().
  • Custom Response Formatting: Implement IntoResponseWith to control how error values convert into Axum responses.
  • Compile-Time Guarantees: Returning undeclared status codes produces a compiler error.
  • OpenAPI / Aide Support: Implement AideResponseFor to automatically document status codes in OpenAPI specifications.

Quick Example

For complete, runnable code, see the examples/ directory.

use axum_error_sets::{
    ApiResultExt, StatusResultExt,
    code::{Conflict, InternalServerError, NotFound, Unauthorized},
};
use common::{AppResultSet, StringError};

fn fetch_user(id: &str) -> AppResultSet<String, (NotFound,)> {
    if id != "valid_id" {
        return Err(StringError::new("user record not found")).into_not_found();
    }
    Ok(String::from("Alice"))
}

fn check_auth(token: &str) -> AppResultSet<(), (Unauthorized,)> {
    if token.is_empty() {
        return Err(StringError::new("missing auth token")).into_unauthorized();
    }
    Ok(())
}

// Low-level error sets expand into a larger contract via `.into_superset()`
fn update_user_profile(
    id: &str,
    token: &str,
    new_name: &str,
) -> AppResultSet<String, (Unauthorized, Conflict, InternalServerError) NotFound> {
    check_auth(token).into_superset()?;
    let mut username = fetch_user(id).into_superset()?;

    if new_name == "taken_username" {
        return Err(StringError::new("username taken")).into_conflict()?;
    }

    username.push_str(" -> ");
    username.push_str(new_name);
    Ok(username)
}