Skip to main content
Lesson 2 of 2Intermediate⏱ 15 mins

DTOs and Validation

Validating incoming JSON.

50% of path

Learning Objectives

  • Use the validator crate

##Never trust the client

Anything that arrives over the network can be wrong, malicious or just missing. A solid API validates input at the edge and keeps its internal data shapes separate from the shapes it sends and receives.

#What is a DTO?

A DTO (data transfer object) is a struct that describes data crossing the API boundary. It is usually different from your database model:

  • A CreateUser request DTO has a name and password, but no id.
  • A UserResponse DTO has an id and name, but never the password hash.
  • The User database model has everything.
code_blocksrust
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct CreateUser {
    pub name: String,
    pub email: String,
    pub password: String,
}

#[derive(Serialize)]
pub struct UserResponse {
    pub id: u32,
    pub name: String,
    pub email: String,
}

Using separate types prevents accidents such as leaking a password hash or letting a client set its own id.

#Validation with the validator crate

Add the crate:

code_blockstoml
validator = { version = "0.18", features = ["derive"] }

Annotate fields with rules:

code_blocksrust
use validator::Validate;

#[derive(Deserialize, Validate)]
pub struct CreateUser {
    #[validate(length(min = 2, max = 50))]
    pub name: String,
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 8))]
    pub password: String,
}

#Validate in the handler

code_blocksrust
use actix_web::{post, web, HttpResponse};

#[post("/users")]
async fn create_user(body: web::Json<CreateUser>) -> Result<HttpResponse, ApiError> {
    body.validate()
        .map_err(|e| ApiError::BadRequest(e.to_string()))?;

    // safe to continue: the data is valid
    Ok(HttpResponse::Created().finish())
}

When validation fails, return 400 (or 422) with a clear message that lists which fields are wrong.

#Layers of validation

  • Syntax: serde rejects invalid JSON and wrong types automatically with a 400.
  • Rules: the validator checks lengths, formats and ranges.
  • Business rules: "email already taken" needs the database, so check it in the service layer.

#Limit input size

Protect your server from huge bodies by configuring a payload limit:

code_blocksrust
web::JsonConfig::default().limit(4096)

Register it with .app_data(...).

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

Break a validation rule.

Starting Code
{"username":"ab","email":"nope","password":"123"}
Suggested Modification

Send that body to your endpoint.

Expected Result

The API returns 400 and mentions the invalid fields.

Explanation: All three rules fail, which shows how validation reports several problems at once.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Returning the database model directly

It is less code, but it exposes every field, including secrets.

Incorrect Pattern
web::Json(user) // user contains password_hash
Correct Pattern
web::Json(UserResponse::from(user))

Explanation: Convert to a response DTO that includes only the fields clients should see.

!

Skipping validation because the frontend validates

Frontend checks can be bypassed with curl or a script.

Incorrect Pattern
// trusting the client
Correct Pattern
body.validate().map_err(|e| ApiError::BadRequest(e.to_string()))?;

Explanation: Always re-validate on the server. Client-side validation is only for user experience.

Practical Exercise

Validate a registration request

Task

Add validation to a RegisterRequest and return 400 for invalid data.

Requirements

  • username length 3 to 20
  • valid email
  • password at least 8 characters

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

Registration, checkout and profile forms in real services use exactly this pattern: strict request DTOs, server-side validation and response DTOs that never contain secrets.

Key Takeaways

  • Use separate request and response structs instead of exposing your database model.
  • The validator crate adds declarative rules like length and email.
  • Return 400 or 422 with a clear message for invalid input.
  • Limit payload size with JsonConfig.

Finished reading?

Mark this lesson as done to track your progress and earn XP.