use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApiError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Unauthorized")]
Unauthorized,
#[error("Forbidden")]
Forbidden,
#[error("Internal server error: {0}")]
InternalServerError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Database error: {0}")]
DatabaseError(#[from] sqlx::Error),
}
impl ApiError {
fn status_code(&self) -> StatusCode {
match self {
ApiError::NotFound(_) => StatusCode::NOT_FOUND,
ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
ApiError::Forbidden => StatusCode::FORBIDDEN,
ApiError::ValidationError(_) => StatusCode::UNPROCESSABLE_ENTITY,
ApiError::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_code(&self) -> &str {
match self {
ApiError::NotFound(_) => "NOT_FOUND",
ApiError::BadRequest(_) => "BAD_REQUEST",
ApiError::Unauthorized => "UNAUTHORIZED",
ApiError::Forbidden => "FORBIDDEN",
ApiError::ValidationError(_) => "VALIDATION_ERROR",
ApiError::InternalServerError(_) => "INTERNAL_SERVER_ERROR",
ApiError::DatabaseError(_) => "DATABASE_ERROR",
}
}
}
#[derive(Serialize)]
struct ErrorResponse {
code: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<String>,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let status_code = self.status_code();
let error_code = self.error_code().to_string();
let message = self.to_string();
tracing::error!(
error_code = %error_code,
status = %status_code,
message = %message,
"API error occurred"
);
let error_response = ErrorResponse {
code: error_code,
message,
details: None,
};
(status_code, Json(error_response)).into_response()
}
}
pub type ApiResult<T> = Result<Json<T>, ApiError>;