Cookbook / Requests and responses

Validation

Check incoming data and answer 422 with a per-field list of problems.

Implement Validate for a request type, then call req.valid_json() (or valid_form() / valid_query()) instead of req.json(). Every failing field is reported at once, so clients can fix the whole form in one round trip.

src/main.rs
use axeon::validate::{is_email, Validate, ValidationErrors};
use axeon::{Json, Server, ServerError};
use serde::Deserialize;

#[derive(Deserialize)]
struct SignUp {
    email: String,
    password: String,
    age: u8,
}

impl Validate for SignUp {
    fn validate(&self) -> Result<(), ValidationErrors> {
        let mut errors = ValidationErrors::new();
        errors.check(is_email(&self.email), "email", "must be an email address");
        errors.check(self.password.len() >= 12, "password", "must be at least 12 characters");
        errors.check(self.age >= 18, "age", "must be 18 or older");
        errors.into_result()
    }
}

fn main() {
    let mut app = Server::new();

    app.post("/signup", |req| async move {
        let form: SignUp = req.valid_json()?;
        Ok::<_, ServerError>((201, Json(form.email)))
    });

    app.listen("127.0.0.1:3000").expect("server failed to start");
}
Terminal
curl -i -X POST http://127.0.0.1:3000/signup \
  -H 'content-type: application/json' \
  -d '{"email":"nope","password":"short","age":12}'

The response is 422 Unprocessable Entity with one entry per field:

JSON
{
  "error": {
    "fields": {
      "age": ["must be 18 or older"],
      "email": ["must be an email address"],
      "password": ["must be at least 12 characters"]
    },
    "message": "Validation failed",
    "status": 422
  }
}

Notes#

  • errors.check(condition, field, message) records a problem when the condition is false. Call it as many times as you like for the same field.
  • Your checks can be any Rust code: query the database, compare two fields, call a regex.