use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct SignupRequest {
pub email: String,
pub password: String,
pub password_confirmation: String,
pub age: i32,
}
#[derive(Debug, Serialize)]
pub struct FieldError {
pub field: &'static str,
pub message: String,
}
#[derive(Debug)]
pub struct NewUser {
pub email: String,
pub password: String,
pub age: u8,
}
use crate::dto::{FieldError, NewUser, SignupRequest};
fn push_err(errors: &mut Vec<FieldError>, field: &'static str, message: &str) {
errors.push(FieldError { field, message: message.to_string() });
}
fn valid_email(value: &str) -> bool {
let parts: Vec<&str> = value.split('@').collect();
parts.len() == 2 && !parts[0].is_empty() && parts[1].contains('.')
}
fn strong_password(value: &str) -> bool {
value.len() >= 8
&& value.chars().any(|c| c.is_ascii_digit())
&& value.chars().any(|c| c.is_ascii_alphabetic())
}
impl SignupRequest {
pub fn validate(self) -> Result<NewUser, Vec<FieldError>> {
let mut errors: Vec<FieldError> = Vec::new();
if !valid_email(&self.email) {
push_err(&mut errors, "email", "must be a valid email address");
}
if !strong_password(&self.password) {
push_err(&mut errors, "password", "must be 8+ chars with letters and digits");
}
if self.password != self.password_confirmation {
push_err(&mut errors, "password_confirmation", "does not match password");
}
if self.age < 13 || self.age > 120 {
push_err(&mut errors, "age", "must be between 13 and 120");
}
if errors.is_empty() {
Ok(NewUser {
email: self.email,
password: self.password,
age: self.age as u8,
})
} else {
Err(errors)
}
}
}
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;
use crate::dto::SignupRequest;
pub async fn signup(Json(payload): Json<SignupRequest>) -> impl IntoResponse {
match payload.validate() {
Ok(new_user) => {
// persist new_user here; the password never reaches the response
let body = json!({ "email": new_user.email, "age": new_user.age });
(StatusCode::CREATED, Json(body))
}
Err(errors) => {
let body = json!({ "errors": errors });
(StatusCode::UNPROCESSABLE_ENTITY, Json(body))
}
}
}
Validating a signup payload well means reporting every problem at once, not bailing on the first bad field. A form that rejects a password, then rejects the email on the next round-trip, frustrates users and wastes requests. This snippet models an error-accumulating validator in Rust, keeping the happy path type-safe while gathering all field errors into a single structured response.
The SignupRequest DTO tab defines the raw inbound shape with serde Deserialize, matching whatever JSON the client sends. It is deliberately "loose" — email, password, and age are plain owned values that may be malformed. Alongside it lives FieldError, a Serialize struct carrying a field name and a human-readable message, so the API can return a flat list the frontend can attach to individual inputs.
The Validator tab holds the core pattern. Rather than returning Result and short-circuiting on the first failure, SignupRequest::validate pushes into a Vec<FieldError> as it checks each rule, then decides at the end. The push_err helper keeps each check to one readable line. Individual predicates like valid_email and password strength are kept small and independent so they can run regardless of what earlier fields did. Only after all checks run does it branch: if errors.is_empty() it constructs a validated NewUser (a distinct type that proves the data is clean), otherwise it returns Err(errors) with the full set. This is the "parse, don't validate" idea — the successful branch yields a different, stronger type, so downstream code cannot accidentally use an unvalidated SignupRequest.
The trade-off is that accumulation requires owning a mutable errors vector and running checks eagerly, versus the terser ? operator that stops early. For user-facing forms that cost is worth it; for internal invariants, fail-fast is usually better.
The HTTP handler tab shows the wiring under axum: the extractor deserializes the body, validate runs, and the result maps to either 201 Created or 422 Unprocessable Entity with the JSON error array. Returning 422 with a machine-readable body lets the client render inline messages without guessing. A subtle pitfall the code avoids is leaking the raw password into logs or responses — only the validated NewUser moves forward, and FieldError messages never echo the submitted secret.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
Share this code
Here's the card — post it anywhere.