A Rust crate for easily defining API-friendly error types with HTTP status codes and user-facing error messages.
When building HTTP APIs, you often need to:
- Convert internal errors into appropriate HTTP status codes
- Provide user-friendly error messages (different from internal error messages)
- Maintain consistency across your error handling
api-error provides a derive macro that automatically implements the ApiError trait for your error types, making it easy to:
- Define custom HTTP status codes for each error variant
- Specify user-facing error messages with formatting
- Integrate seamlessly with Axum (optional feature)
- Forward errors transparently through your error hierarchy
use api_error::ApiError;
use http::StatusCode;
#[derive(Debug, thiserror::Error, ApiError)]
enum MyError {
#[error("Invalid input")]
#[api_error(status_code = 400, message = "The provided input is invalid")]
InvalidInput,
#[error("Resource not found")]
#[api_error(status_code = 404, message = "The requested resource was not found")]
NotFound,
#[error("Internal error")]
#[api_error(status_code = StatusCode::INTERNAL_SERVER_ERROR)]
Internal,
}
let err = MyError::InvalidInput;
assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(err.message().as_ref(), "The provided input is invalid");
assert_eq!(err.to_string(), "Invalid input"); // From thiserroruse api_error::ApiError;
#[derive(Debug, thiserror::Error, ApiError)]
enum AppError {
// Unnamed fields with positional formatting
#[error("Database error: {0}")]
#[api_error(status_code = 500, message = "Database operation failed: {0}")]
Database(String),
// Named fields with named formatting
#[error("Validation failed on {field}")]
#[api_error(status_code = 422, message = "Field `{field}` has invalid value")]
Validation { field: String, value: String },
}
let err = AppError::Database("Connection timeout".to_string());
assert_eq!(err.status_code().as_u16(), 500);
assert_eq!(err.message().as_ref(), "Database operation failed: Connection timeout");
let err = AppError::Validation {
field: "email".to_string(),
value: "invalid".to_string(),
};
assert_eq!(err.status_code().as_u16(), 422);
assert_eq!(err.message().as_ref(), "Field `email` has invalid value");use api_error::ApiError;
#[derive(Debug, thiserror::Error, ApiError)]
#[error("Authentication failed: {reason}")]
#[api_error(status_code = 401, message = "Authentication failed")]
struct AuthError {
reason: String,
}
let err = AuthError {
reason: "Invalid token".to_string(),
};
assert_eq!(err.status_code().as_u16(), 401);
assert_eq!(err.message().as_ref(), "Authentication failed");Use message(inherit) to use the Display implementation as the user-facing message:
use api_error::ApiError;
#[derive(Debug, thiserror::Error, ApiError)]
enum MyError {
#[error("User-friendly error message")]
#[api_error(message(inherit), status_code = 400)]
BadRequest,
}
let err = MyError::BadRequest;
assert_eq!(err.message().as_ref(), "User-friendly error message");Forward both status code and message from an inner error:
use api_error::ApiError;
#[derive(Debug, thiserror::Error, ApiError)]
#[error("Database error")]
#[api_error(status_code = 503, message = "Service temporarily unavailable")]
struct DatabaseError;
#[derive(Debug, thiserror::Error, ApiError)]
enum AppError {
#[error(transparent)]
#[api_error(transparent)]
Database(DatabaseError),
#[error("Other error")]
#[api_error(status_code = 500, message = "Internal error")]
Other,
}
let err = AppError::Database(DatabaseError);
assert_eq!(err.status_code().as_u16(), 503); // Forwarded from DatabaseError
assert_eq!(err.message().as_ref(), "Service temporarily unavailable");With the axum feature enabled, ApiError types automatically implement IntoResponse:
use api_error::ApiError;
use axum::{Router, routing::get};
#[derive(Debug, thiserror::Error, ApiError)]
enum MyApiError {
#[error("Not found")]
#[api_error(status_code = 404, message = "Resource not found")]
NotFound,
}
async fn handler() -> Result<String, MyApiError> {
Err(MyApiError::NotFound)
}
let app: Router = Router::new().route("/", get(handler));
// Returns JSON response:
// Status: 404
// Body: {"message": "Resource not found"}