{% extends "docs_base.html" %}
{% block content %}
<div class="prose prose-slate max-w-none">
<h1>Data Transfer Objects (DTOs)</h1>
<p>In Ruskit, Data Transfer Objects (DTOs) are structures that define how data should be sent over the network. They help in validating incoming requests and structuring outgoing responses.</p>
<h2>Overview</h2>
<p>DTOs serve several purposes:</p>
<ul>
<li>Separate the API contract from internal data structures</li>
<li>Provide input validation for requests</li>
<li>Control what data is exposed in responses</li>
<li>Enable versioning of API responses</li>
</ul>
<h2>Structure</h2>
<p>A typical DTO module contains three main structs:</p>
<ol>
<li>Request DTO - For handling incoming data</li>
<li>Response DTO - For sending data back to the client</li>
<li>List Response DTO - For sending collections of data</li>
</ol>
<h3>Example</h3>
<pre><code class="language-rust">use serde::{Serialize, Deserialize};
use validator::Validate;
use crate::app::models::User;
// Response DTO
#[derive(Serialize)]
pub struct UserResponse {
pub id: i64,
pub name: String,
pub email: String,
pub created_at: i64,
pub updated_at: i64,
}
// Request DTO with validation
#[derive(Deserialize, Validate)]
pub struct CreateUserRequest {
#[validate(length(min = 3, max = 50))]
pub name: String,
#[validate(email)]
pub email: String,
#[validate(length(min = 8))]
pub password: String,
}
// List Response DTO
#[derive(Serialize)]
pub struct UserListResponse {
pub data: Vec<UserResponse>,
}</code></pre>
<h2>Creating DTOs</h2>
<p>Use the <code>make:dto</code> command to generate a new DTO:</p>
<pre><code class="language-bash">cargo kit make:dto User</code></pre>
<p>This will create:</p>
<ul>
<li><code>src/app/dtos/user.rs</code> with basic DTO structures</li>
<li>Update <code>src/app/dtos/mod.rs</code> to include the new module</li>
</ul>
<h2>Request DTOs</h2>
<p>Request DTOs handle incoming data and validation:</p>
<pre><code class="language-rust">#[derive(Deserialize, Validate)]
pub struct CreateUserRequest {
#[validate(length(min = 3, max = 50))]
pub name: String,
#[validate(email)]
pub email: String,
#[validate(length(min = 8))]
pub password: String,
#[validate(range(min = 0, max = 150))]
pub age: Option<i32>,
}</code></pre>
<h3>Validation Rules</h3>
<p>Common validation attributes:</p>
<ul>
<li><code>length(min = x, max = y)</code> - String length constraints</li>
<li><code>range(min = x, max = y)</code> - Numeric range constraints</li>
<li><code>email</code> - Email format validation</li>
<li><code>url</code> - URL format validation</li>
<li><code>contains(pattern = "x")</code> - String contains pattern</li>
<li><code>regex(path = "REGEX")</code> - Regular expression matching</li>
</ul>
<h2>Response DTOs</h2>
<p>Response DTOs control what data is sent to clients:</p>
<pre><code class="language-rust">#[derive(Serialize)]
pub struct UserResponse {
pub id: i64,
pub name: String,
pub email: String,
// Note: password is not included in response
pub created_at: i64,
pub updated_at: i64,
}</code></pre>
<h3>List Responses</h3>
<p>For collections of data:</p>
<pre><code class="language-rust">#[derive(Serialize)]
pub struct UserListResponse {
pub data: Vec<UserResponse>,
pub total: usize,
pub page: usize,
pub per_page: usize,
}</code></pre>
<h2>Model Conversion</h2>
<p>DTOs should implement <code>From</code> traits for conversion:</p>
<pre><code class="language-rust">// Convert from Model to Response
impl From<User> for UserResponse {
fn from(user: User) -> Self {
Self {
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at,
updated_at: user.updated_at,
}
}
}
// Convert from Request to Model
impl From<CreateUserRequest> for User {
fn from(req: CreateUserRequest) -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
Self {
id: 0, // New user, ID will be set by database
name: req.name,
email: req.email,
password_hash: hash_password(&req.password),
created_at: now,
updated_at: now,
}
}
}</code></pre>
<h2>Best Practices</h2>
<ol>
<li>
<strong>Separation of Concerns</strong>
<ul>
<li>Keep DTOs focused on data transfer</li>
<li>Don't include business logic in DTOs</li>
<li>Use separate DTOs for different API versions</li>
</ul>
</li>
<li>
<strong>Validation</strong>
<ul>
<li>Always validate incoming data</li>
<li>Use appropriate validation rules</li>
<li>Consider optional fields when appropriate</li>
</ul>
</li>
<li>
<strong>Security</strong>
<ul>
<li>Never expose sensitive data in responses</li>
<li>Validate all incoming data</li>
<li>Use appropriate serialization attributes</li>
</ul>
</li>
<li>
<strong>Naming Conventions</strong>
<ul>
<li>Use PascalCase for struct names</li>
<li>End request DTOs with <code>Request</code></li>
<li>End response DTOs with <code>Response</code></li>
<li>Use descriptive names for fields</li>
</ul>
</li>
<li>
<strong>Documentation</strong>
<ul>
<li>Document all DTO fields</li>
<li>Include validation requirements</li>
<li>Explain any special formatting</li>
</ul>
</li>
</ol>
<h2>Usage in Controllers</h2>
<pre><code class="language-rust">use axum::{
response::Json,
extract::Path,
};
use crate::app::dtos::user::{CreateUserRequest, UserResponse};
impl UserController {
pub async fn store(
Json(payload): Json<CreateUserRequest>
) -> Json<UserResponse> {
// Payload is already validated due to DTO
let user: User = payload.into();
let created = User::create(user).await?;
Json(UserResponse::from(created))
}
}</code></pre>
</div>
{% endblock %}