#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmailAddress(String);
impl EmailAddress {
pub fn parse(raw: &str) -> Result<Self, String> {
if raw.contains('@') && !raw.starts_with('@') {
Ok(EmailAddress(raw.to_string()))
} else {
Err(format!("invalid email address: {raw}"))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Role {
Admin,
Member,
Guest,
}
#[derive(Debug, Clone)]
pub struct User {
pub id: u64,
pub email: EmailAddress,
pub display_name: String,
pub role: Role,
}
use serde::{Deserialize, Serialize};
use crate::domain::{EmailAddress, Role, User};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserDto {
pub id: u64,
pub email: String,
pub display_name: String,
pub role: String,
}
#[derive(Debug)]
pub struct ConversionError(pub String);
impl From<Role> for String {
fn from(role: Role) -> Self {
match role {
Role::Admin => "admin".to_string(),
Role::Member => "member".to_string(),
Role::Guest => "guest".to_string(),
}
}
}
impl From<User> for UserDto {
fn from(user: User) -> Self {
UserDto {
id: user.id,
email: user.email.as_str().to_string(),
display_name: user.display_name,
role: user.role.into(),
}
}
}
impl TryFrom<UserDto> for User {
type Error = ConversionError;
fn try_from(dto: UserDto) -> Result<Self, Self::Error> {
let email = EmailAddress::parse(&dto.email).map_err(ConversionError)?;
let role = match dto.role.as_str() {
"admin" => Role::Admin,
"member" => Role::Member,
"guest" => Role::Guest,
other => return Err(ConversionError(format!("unknown role: {other}"))),
};
Ok(User {
id: dto.id,
email,
display_name: dto.display_name,
role,
})
}
}
use axum::{http::StatusCode, response::IntoResponse, Json};
use crate::domain::User;
use crate::dto::UserDto;
pub async fn create_user(Json(payload): Json<UserDto>) -> impl IntoResponse {
let user = match User::try_from(payload) {
Ok(user) => user,
Err(err) => {
return (StatusCode::BAD_REQUEST, Json(err.0)).into_response();
}
};
let saved = persist(user).await;
// Infallible domain -> DTO conversion via the derived Into impl.
let body: UserDto = saved.into();
(StatusCode::CREATED, Json(body)).into_response()
}
async fn persist(mut user: User) -> User {
user.id = 42;
user
}
This snippet shows a common layering technique in Rust web services: keeping the internal domain model separate from the wire-facing DTO, and bridging the two exclusively through the standard From trait. The idea is that the domain type in domain.rs — User with a strongly-typed EmailAddress newtype and a Role enum — should never be serialized directly. Serialization concerns (field naming, string encodings, optional fields) belong to the DTO, so the two can evolve independently without leaking database or business details onto the API surface.
In domain.rs, EmailAddress is a newtype wrapper whose only constructor, parse, enforces an invariant (a present @). This makes an invalid email unrepresentable inside the domain, which is why conversions in one direction can be infallible while the other must be fallible. The Role enum captures the closed set of allowed roles.
In dto.rs, UserDto derives Serialize/Deserialize and uses serde attributes like rename_all = "camelCase" to shape the JSON contract. The outbound direction implements From<User> for UserDto: since a valid User always holds valid data, this conversion cannot fail, so From (not TryFrom) is the right trait. Roles map through a small impl From<Role> for String. The inbound direction is different: raw JSON can contain a malformed email or unknown role, so it implements TryFrom<UserDto> for User with a ConversionError, because parsing untrusted input is inherently fallible. Choosing From versus TryFrom deliberately encodes which conversions can fail.
In handler.rs, an axum handler consumes these conversions. create_user accepts a Json<UserDto>, calls User::try_from to validate and lift into the domain, and returns errors as 400 responses. On the way out it relies on .into() — the free Into impl that Rust derives from From — to convert the domain result back to a UserDto for the JSON response. The payoff is that handlers stay thin and every boundary crossing is a single, type-checked .into() or try_into(), with validation centralized in one place and the compiler guaranteeing no domain type is accidentally exposed.
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.