A Rust library for standardized API error handling with automatic module-based error code management and message formatting.
- Automatic Module Prefixing: Error messages automatically include module paths in
[module] messageformat - Auto Error Code Numbering: Sequential error code allocation based on module and base code
- Transparent Error Delegation: Proper error propagation through
transparentvariants - HTTP Status Code Support: Attach HTTP status codes to error variants
- Web Framework Integration: Built-in support for Axum, Actix-web, and Poem
- Type-safe: Compile-time error code and module validation
- Zero Runtime Cost: All formatting logic uses efficient trait methods
Add to your Cargo.toml:
[dependencies]
apiresponse = "0.1"
thiserror = "1.0" # For error definitionsFor web framework integration:
[dependencies]
apiresponse = { version = "0.1", features = ["axum"] } # or "actix", "poem"use api_response::Response;
use thiserror::Error;
#[derive(Debug, Error, Response)]
#[response(module = "auth", base = 1000)]
pub enum AuthError {
#[error("User not found")]
#[response(status = 404)]
UserNotFound,
#[error("Invalid password")]
#[response(status = 401)]
InvalidPassword,
}let error = AuthError::UserNotFound;
// Automatic module prefix
println!("{}", error.message());
// Output: [auth] User not found
// Error code (auto-numbered)
println!("{}", error.error_code());
// Output: 1000
// HTTP status code
println!("{}", error.http_status_code());
// Output: 404use apiresponse::ApiResponse;
// Method 1: From error
let response = ApiResponse::from_error(error);
// Method 2: From Result
let result: Result<String, AuthError> = Err(AuthError::UserNotFound);
let response: ApiResponse = result.into();
// Serialize to JSON
let json = serde_json::to_string(&response).unwrap();
// {
// "code": 1000,
// "message": "[auth] User not found",
// "data": null
// }Error messages automatically include module paths:
#[derive(Debug, Error, Response)]
#[response(module = "payment.stripe", base = 2000)]
pub enum PaymentError {
#[error("Insufficient balance")]
InsufficientBalance, // Error code: 2000
}
let err = PaymentError::InsufficientBalance;
err.message() // → "[payment.stripe] Insufficient balance"
err.module_path() // → "payment.stripe"
err.raw_message() // → "Insufficient balance"Proper error propagation when wrapping errors:
#[derive(Debug, Error, Response)]
#[response(module = "api")]
pub enum ApiError {
#[error(transparent)]
#[response(transparent)]
Auth(#[from] AuthError), // Delegates to inner error
}
// When ApiError::Auth(AuthError::UserNotFound) occurs:
let error = ApiError::Auth(AuthError::UserNotFound);
error.message() // → "[auth] User not found" (from AuthError)
error.module_path() // → "auth" (from AuthError)
error.error_code() // → 1000 (from AuthError)#[derive(Debug, Error, Response)]
#[response(module = "user", base = 1000)]
pub enum UserError {
#[error("Not found")]
NotFound, // Code: 1000
#[error("Unauthorized")]
Unauthorized, // Code: 1001
#[error("Forbidden")]
Forbidden, // Code: 1002
}#[derive(Debug, Error, Response)]
#[response(module = "validation")]
pub enum ValidationError {
#[error("Invalid email")]
#[response(code = 4001, status = 400)]
InvalidEmail,
#[error("Weak password")]
#[response(code = 4002, status = 400)]
WeakPassword,
}#[derive(Debug, Error, Response)]
#[response(module = "auth", base = 1000)]
pub enum AuthError {
#[error("User not found")]
NotFound, // Code: 1000 (auto)
#[error("Locked")]
#[response(code = 1099)] // Override: 1099 instead of 1001
Locked,
}#[derive(Debug, Error, Response)]
#[response(base = 6000)]
pub enum SimpleError {
#[error("Generic error")]
GenericError, // Code: 6000, no module prefix
#[error("Another error")]
AnotherError, // Code: 6001, no module prefix
}
// Usage
let err = SimpleError::GenericError;
err.message() // → "Generic error" (no module prefix)
err.module_path() // → ""#[derive(Debug, Error, Response)]
#[response(module = "user", base = 1000)]
pub enum UserError {
#[error("User not found")]
#[response(status = 404)]
NotFound,
#[error("Permission denied")]
#[response(status = 403)]
PermissionDenied,
}
// In API handler
async fn get_user(id: u64) -> Result<Json<User>, UserError> {
let user = find_user(id).ok_or(UserError::NotFound)?;
check_permission(&user).ok_or(UserError::PermissionDenied)?;
Ok(Json(user))
}#[derive(Debug, Error, Response)]
#[response(module = "app")]
pub enum AppError {
#[error(transparent)]
#[response(transparent)]
User(#[from] UserError),
#[error(transparent)]
#[response(transparent)]
Database(#[from] DbError),
#[error("Internal error")]
#[response(code = 5000, status = 500)]
Internal,
}use axum::{routing::get, Router};
use apiresponse::ApiResponse;
async fn handler() -> ApiResponse {
// ApiResponse automatically implements IntoResponse
// HTTP status code is set from the status_code field
ApiResponse::from_error(AuthError::UserNotFound)
}
let app = Router::new().route("/user", get(handler));use actix_web::{get, HttpResponse};
use apiresponse::ApiResponse;
#[get("/user")]
async fn handler() -> ApiResponse {
// ApiResponse implements Responder
ApiResponse::from_error(AuthError::UserNotFound)
}use poem::{get, handler, Route};
use api_response::ApiResponse;
#[handler]
async fn handler() -> ApiResponse {
// ApiResponse implements IntoResponse
ApiResponse::from_error(AuthError::UserNotFound)
}
let app = Route::new().at("/user", get(handler));We provide comprehensive examples:
# Complete feature demonstration
cargo run --example complete_example
# Web framework integration
cargo run --example web_framework_examplepub trait Response: Display {
/// Returns the error code
fn error_code(&self) -> u64;
/// Returns the module path (e.g., "auth.login")
fn module_path(&self) -> &'static str;
/// Returns the raw error message without module prefix
fn raw_message(&self) -> String;
/// Returns the formatted message with module prefix: "[module] message"
fn message(&self) -> String;
/// Returns the HTTP status code
fn http_status_code(&self) -> u16;
}#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiResponse {
pub code: u64,
pub message: String,
pub data: serde_json::Value,
#[serde(skip)]
pub status_code: u16,
}Methods:
ApiResponse::success(data)- Create success response with dataApiResponse::success_with_message(data, message)- Success with custom messageApiResponse::ok()- Create empty success response
#[response(module = "module_name", base = base_code)]
// ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^
// Optional (default "") Optional (if omitted, variants must specify code)#[response(code = error_code, status = http_status, message = "custom_message")]
// ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
// Optional (override) Optional (default 200) Optional (override Display)
#[response(transparent)] // Delegate to inner error// Get module path
let module = error.module_path(); // "auth.login"
// Raw vs Formatted messages
error.raw_message() // → "User not found"
error.message() // → "[auth.login] User not found"#[response(module = "api.v1.auth")]
pub enum AuthError { /* ... */ }
// Messages will be prefixed with [api.v1.auth]- Simple by Default: Minimal boilerplate, maximum functionality
- Type Safety: Compile-time validation of error codes and modules
- Flexibility: Support both auto-numbering and manual code assignment
- Zero Cost: All abstractions compile away to efficient code
- Framework Agnostic: Core library has no web framework dependencies
- Rust 1.70 or later
thiserrorfor error definitions (recommended)serdeandserde_jsonfor serialization
MIT OR Apache-2.0
Contributions are welcome! Please feel free to submit a Pull Request.