Errors & validation

A single error type, mapped once to the right HTTP status and a consistent JSON body. DB errors are logged but never leak their details to the client.

The error enum #

// errors.rs
#[derive(Debug, thiserror::Error)]
pub enum MemoError {
    #[error("Non authentifié")]            Unauthorized,
    #[error("Introuvable: {0}")]           NotFound(String),
    #[error("Données invalides: {0}")]     Validation(String),
    #[error("Erreur base de données")]     Database(#[from] sqlx::Error),
    #[error("Erreur interne")]             Internal(#[from] anyhow::Error),
}

impl IntoResponse for MemoError {
    fn into_response(self) -> Response {
        let (status, code) = match &self {
            MemoError::Unauthorized  => (StatusCode::UNAUTHORIZED,          "UNAUTHORIZED"),
            MemoError::NotFound(_)   => (StatusCode::NOT_FOUND,             "NOT_FOUND"),
            MemoError::Validation(_) => (StatusCode::UNPROCESSABLE_ENTITY,  "VALIDATION"),
            MemoError::Database(e)   => { tracing::error!(error=%e, "db"); (StatusCode::INTERNAL_SERVER_ERROR, "DATABASE_ERROR") }
            MemoError::Internal(e)   => { tracing::error!(error=%e, "internal"); (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR") }
        };
        (status, Json(json!({ "error": code, "message": self.to_string() }))).into_response()
    }
}

pub type Result<T> = std::result::Result<T, MemoError>;
ErrorStatus
Unauthorized401
NotFound404
Validation422
Database / Internal500

Input validation #

#[derive(Deserialize, validator::Validate)]
pub struct CreateNoteDto {
    #[validate(length(min = 1, max = 500))]
    pub title: String,
    pub body:  String,
}

// dans le handler create :
dto.validate().map_err(|e| MemoError::Validation(e.to_string()))?;

Returning an error — in other languages #

// Rust — via IntoResponse (cf. l'enum ci-dessus)
return (
    StatusCode::NOT_FOUND,
    Json(json!({ "error": "NOT_FOUND", "message": "Note introuvable" })),
).into_response();
<?php
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['error' => 'NOT_FOUND', 'message' => 'Note introuvable']);
exit;
# Python — Flask
return jsonify(error="NOT_FOUND", message="Note introuvable"), 404
// Go
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{
    "error": "NOT_FOUND", "message": "Note introuvable",
})
# Perl — Mojolicious
return $c->render(status => 404,
    json => { error => "NOT_FOUND", message => "Note introuvable" });
Rule

Always validate before any DB operation; on public routes, don't distinguish "not found" from "unauthorized".