Validation and errors

Use validation to reject an input before it changes application state. Use an application error to explain which kind of failure occurred without making your service depend on HTTP status codes.

Define a rule on the input

This complete input type can live in a module shared by the HTTP adapter and the application service. It requires iet-core and Serde with its derive feature.

rust
use iet_core::{AppError, AppResult, ErrorKind, FieldError, Validate};
use serde::Deserialize;

#[derive(Deserialize)]
pub struct CreateNote {
    pub text: String,
}

impl Validate for CreateNote {
    fn validate(&self) -> AppResult<()> {
        if self.text.trim().is_empty() {
            let mut error = AppError::new(
                ErrorKind::Validation,
                "note.text_required",
                "A note needs some text",
            );
            error.fields.push(FieldError {
                path: "text".into(),
                code: "required".into(),
            });
            return Err(error);
        }
        Ok(())
    }
}

The rule belongs to the input, not to an Axum extractor. A command-line adapter or a test can use the same rule. Keep submitted values out of error codes and field metadata: a field name such as text is useful; repeating a password or private note in the response is not.

Validate at both boundaries

iet_http_axum::ValidatedJson<CreateNote> parses JSON and calls validate(). That protects the HTTP boundary. The service must still call input.validate()? before mutation, because not every caller goes through HTTP.

This is deliberate duplication of the check, not duplication of the rule. One implementation of Validate is called in both places.

JSON parsing errors are different from domain validation errors:

Request problemHTTP result
Malformed JSON400, code http.json
JSON has the wrong data shape for T422, code http.json
Missing or unsupported JSON content type415, code http.content_type
JSON exceeds the configured limit413, code http.body_limit when rejected by the extractor
JSON parses but Validate fails422, your validation code

Outer body-limit middleware can reject a request before the extractor runs; do not assume every early middleware rejection has the extractor's problem body.

Convert errors only at the HTTP edge

Services return AppResult<T>. A handler returns a result whose error is iet_http_axum::ApiError; its From<AppError> implementation allows ? to do the conversion. The notes tutorial shows this in a handler.

ApiError produces application/problem+json with type, title, status, code, and errors. It does not serialize the application's free-form message. The validation error above becomes:

json
{
  "type": "about:blank",
  "title": "Validation failed",
  "status": 422,
  "code": "note.text_required",
  "errors": [{"path": "text", "code": "required"}]
}

Choose an error kind

ErrorKindStatusUse when
Validation422A well-formed input violates an application rule
Unauthenticated401The operation requires an authenticated caller
Forbidden403The caller cannot perform the operation
NotFound404The requested resource is not available to this lookup
Conflict409The requested change conflicts with current state
Unavailable503A required service cannot currently complete the operation
DeadlineExceeded504The operation exceeded its deadline
Internal500An unexpected internal condition prevented completion

For server errors, ApiError returns the generic code service.error and no field errors. Diagnostics are logged server-side instead of exposed to clients. Do not put secrets in an error message merely because it is not sent over HTTP: it may still be logged.

Source contract: crates/core/iet-core/src/lib.rs; HTTP behavior: crates/features/iet-http-axum/src/extract.rs and crates/features/iet-http-axum/src/error.rs.

IET Framework documentation — built with the IET Framework.