Skip to main content

axum_api_kit/
health.rs

1use axum::{
2    http::StatusCode,
3    response::{IntoResponse, Response},
4    Json,
5};
6use serde::Serialize;
7
8/// A minimal health-check response body.
9///
10/// Serializes as `{ "status": "ok" }` and responds with `200 OK`.
11///
12/// Use [`HealthResponse::degraded`] or [`HealthResponse::unhealthy`] for non-nominal states.
13///
14/// # Example
15///
16/// ```rust
17/// use axum::response::IntoResponse;
18/// use axum_api_kit::HealthResponse;
19///
20/// async fn health() -> impl IntoResponse {
21///     HealthResponse::ok()
22/// }
23/// ```
24#[derive(Debug, Clone, Serialize)]
25#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26pub struct HealthResponse {
27    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "ok"))]
28    pub status: &'static str,
29    #[serde(skip)]
30    status_code: StatusCode,
31}
32
33impl HealthResponse {
34    /// Returns a `HealthResponse` with `status = "ok"` and HTTP `200 OK`.
35    pub const fn ok() -> Self {
36        Self {
37            status: "ok",
38            status_code: StatusCode::OK,
39        }
40    }
41
42    /// Returns a `HealthResponse` with `status = "degraded"` and HTTP `200 OK`.
43    ///
44    /// Use when the service is reachable but operating in a reduced capacity
45    /// (e.g., a non-critical dependency is down).
46    pub const fn degraded() -> Self {
47        Self {
48            status: "degraded",
49            status_code: StatusCode::OK,
50        }
51    }
52
53    /// Returns a `HealthResponse` with `status = "unhealthy"` and HTTP `503 Service Unavailable`.
54    ///
55    /// Use when the service cannot fulfill requests normally
56    /// (e.g., the primary database is unreachable).
57    pub const fn unhealthy() -> Self {
58        Self {
59            status: "unhealthy",
60            status_code: StatusCode::SERVICE_UNAVAILABLE,
61        }
62    }
63}
64
65impl IntoResponse for HealthResponse {
66    fn into_response(self) -> Response {
67        (self.status_code, Json(self)).into_response()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn ok_has_correct_status() {
77        let r = HealthResponse::ok();
78        assert_eq!(r.status, "ok");
79        assert_eq!(r.status_code, StatusCode::OK);
80    }
81
82    #[test]
83    fn degraded_has_correct_status() {
84        let r = HealthResponse::degraded();
85        assert_eq!(r.status, "degraded");
86        assert_eq!(r.status_code, StatusCode::OK);
87    }
88
89    #[test]
90    fn unhealthy_has_correct_status() {
91        let r = HealthResponse::unhealthy();
92        assert_eq!(r.status, "unhealthy");
93        assert_eq!(r.status_code, StatusCode::SERVICE_UNAVAILABLE);
94    }
95
96    #[test]
97    fn serializes_status_field_only() {
98        let r = HealthResponse::ok();
99        let v = serde_json::to_value(&r).unwrap();
100        assert_eq!(v["status"], "ok");
101        assert!(v.get("status_code").is_none());
102    }
103}