1use axum::{
2 http::StatusCode,
3 response::{IntoResponse, Response},
4 Json,
5};
6use serde::Serialize;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum ApiError {
12 #[error("Not found: {0}")]
13 NotFound(String),
14
15 #[error("Bad request: {0}")]
16 BadRequest(String),
17
18 #[error("Unauthorized")]
19 Unauthorized,
20
21 #[error("Forbidden")]
22 Forbidden,
23
24 #[error("Internal server error: {0}")]
25 InternalServerError(String),
26
27 #[error("Validation error: {0}")]
28 ValidationError(String),
29
30 #[error("Database error: {0}")]
31 DatabaseError(#[from] sqlx::Error),
32}
33
34impl ApiError {
35 fn status_code(&self) -> StatusCode {
36 match self {
37 ApiError::NotFound(_) => StatusCode::NOT_FOUND,
38 ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
39 ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
40 ApiError::Forbidden => StatusCode::FORBIDDEN,
41 ApiError::ValidationError(_) => StatusCode::UNPROCESSABLE_ENTITY,
42 ApiError::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
43 ApiError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
44 }
45 }
46
47 fn error_code(&self) -> &str {
48 match self {
49 ApiError::NotFound(_) => "NOT_FOUND",
50 ApiError::BadRequest(_) => "BAD_REQUEST",
51 ApiError::Unauthorized => "UNAUTHORIZED",
52 ApiError::Forbidden => "FORBIDDEN",
53 ApiError::ValidationError(_) => "VALIDATION_ERROR",
54 ApiError::InternalServerError(_) => "INTERNAL_SERVER_ERROR",
55 ApiError::DatabaseError(_) => "DATABASE_ERROR",
56 }
57 }
58}
59
60#[derive(Serialize)]
61struct ErrorResponse {
62 code: String,
63 message: String,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 details: Option<String>,
66}
67
68impl IntoResponse for ApiError {
69 fn into_response(self) -> Response {
70 let status_code = self.status_code();
71 let error_code = self.error_code().to_string();
72 let message = self.to_string();
73
74 tracing::error!(
76 error_code = %error_code,
77 status = %status_code,
78 message = %message,
79 "API error occurred"
80 );
81
82 let error_response = ErrorResponse {
83 code: error_code,
84 message,
85 details: None,
86 };
87
88 (status_code, Json(error_response)).into_response()
89 }
90}
91
92pub type ApiResult<T> = Result<Json<T>, ApiError>;