Rust Concepts: dyn Trait, Custom Iterators, Deref/Drop & Axum REST API (Part 5)

Rust Concepts: dyn Trait, Custom Iterators, Deref/Drop & Axum REST API (Part 5)

posted Originally published at dev.to 9 min read

title: Rust Concepts: dyn Trait, Custom Iterators, Deref/Drop & Axum REST API (Part 5)
published: true
tags: rust, webdev, intermediate, systems
cover_image: https://images.unsplash.com/photo-1504639725590-34d0984388bd?w=1000

series: Core Rust Concepts

This is Part 5 of the Core Rust Concepts series.

  • Part 1 — Ownership, Borrowing, Lifetimes, Traits, Result/Option, Pattern Matching
  • Part 2 — Closures, Iterators, Generics, Enums, Smart Pointers, Async/Await
  • Part 3 — Macros, Modules, Testing, Unsafe Rust, FFI
  • Part 4 — Threads, Channels, Send/Sync, Mutex, Atomics, clap

Table of Contents

  1. Trait Objects and Dynamic Dispatch
  2. The Deref Trait
  3. The Drop Trait
  4. Custom Iterators
  5. Building a REST API with Axum

25. Trait Objects and Dynamic Dispatch

In Part 2 we used impl Trait — that's static dispatch: the compiler knows the exact type at compile time and monomorphizes it. Sometimes you don't know the type until runtime. That's where trait objects (dyn Trait) come in.

Static vs Dynamic dispatch

trait Greet {
    fn hello(&self) -> String;
}

struct English;
struct Spanish;

impl Greet for English {
    fn hello(&self) -> String { String::from("Hello!") }
}

impl Greet for Spanish {
    fn hello(&self) -> String { String::from("¡Hola!") }
}

// Static dispatch — type known at compile time, zero overhead
fn greet_static(g: &impl Greet) {
    println!("{}", g.hello());
}

// Dynamic dispatch — type known at runtime via vtable
fn greet_dynamic(g: &dyn Greet) {
    println!("{}", g.hello());
}

fn main() {
    let e = English;
    let s = Spanish;

    greet_static(&e);
    greet_dynamic(&s);

    // Only dyn Trait allows mixing types in a collection
    let greeters: Vec<Box<dyn Greet>> = vec![
        Box::new(English),
        Box::new(Spanish),
        Box::new(English),
    ];

    for g in &greeters {
        println!("{}", g.hello());
    }
}

When to use dyn Trait vs impl Trait

impl Trait dyn Trait
Dispatch Static (compile time) Dynamic (runtime vtable)
Performance Zero overhead Small indirection cost
Heterogeneous collections
Return from function ✅ (one concrete type) ✅ (any type at runtime)
Sized ❌ (must be behind & or Box)

Object-safe traits

Not every trait can be made into a trait object. A trait is object-safe if:

  • It has no methods that return Self
  • It has no generic methods
// ✅ Object-safe
trait Drawable {
    fn draw(&self);
    fn area(&self) -> f64;
}

// ❌ NOT object-safe — returns Self
trait Clone2 {
    fn clone2(&self) -> Self; // can't use as dyn Clone2
}

Real-world pattern — plugin system

trait Plugin {
    fn name(&self) -> &str;
    fn run(&self, input: &str) -> String;
}

struct UppercasePlugin;
struct ReversePlugin;

impl Plugin for UppercasePlugin {
    fn name(&self) -> &str { "uppercase" }
    fn run(&self, input: &str) -> String { input.to_uppercase() }
}

impl Plugin for ReversePlugin {
    fn name(&self) -> &str { "reverse" }
    fn run(&self, input: &str) -> String { input.chars().rev().collect() }
}

struct Pipeline {
    plugins: Vec<Box<dyn Plugin>>,
}

impl Pipeline {
    fn new() -> Self { Self { plugins: vec![] } }

    fn add(mut self, p: Box<dyn Plugin>) -> Self {
        self.plugins.push(p);
        self
    }

    fn run(&self, input: &str) -> String {
        self.plugins.iter().fold(input.to_string(), |acc, p| p.run(&acc))
    }
}

fn main() {
    let pipeline = Pipeline::new()
        .add(Box::new(UppercasePlugin))
        .add(Box::new(ReversePlugin));

    println!("{}", pipeline.run("hello")); // "OLLEH"
}

26. The Deref Trait

Deref controls what happens when you use the * operator on a type. It's also what enables deref coercions — Rust automatically calls deref() to convert types so you don't have to do it manually.

use std::ops::Deref;

// A simple smart pointer
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> { MyBox(x) }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0 // return a reference to the inner value
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let x = MyBox::new(String::from("Rustacean"));

    // These are all equivalent due to deref coercions:
    hello(&x);           // MyBox<String> → &String → &str  (auto!)
    hello(&(*x)[..]);    // manual — what Rust does behind the scenes
    hello(x.deref());    // explicit deref
}

Deref coercion chain

Rust automatically chains Deref calls as many times as needed:

MyBox<String>  →  String  →  str
    &MyBox<String>  →  &String  →  &str
fn main() {
    let boxed = Box::new(String::from("hello"));

    // Box<String> derefs to String, then to str
    println!("{}", boxed.to_uppercase()); // HELLO
    println!("{}", boxed.len());          // 5

    let s: &str = &boxed; // Box<String> → &String → &str — all automatic
    println!("{s}");
}

DerefMut

DerefMut does the same for mutable contexts:

use std::ops::{Deref, DerefMut};

struct Wrapper(Vec<i32>);

impl Deref for Wrapper {
    type Target = Vec<i32>;
    fn deref(&self) -> &Vec<i32> { &self.0 }
}

impl DerefMut for Wrapper {
    fn deref_mut(&mut self) -> &mut Vec<i32> { &mut self.0 }
}

fn main() {
    let mut w = Wrapper(vec![1, 2, 3]);
    w.push(4);                     // DerefMut lets you call Vec methods
    println!("{:?}", w.as_slice()); // Deref: [1, 2, 3, 4]
}

27. The Drop Trait

Drop lets you run custom cleanup code when a value goes out of scope — like a destructor in C++. Rust calls drop() automatically; you never call it yourself (use std::mem::drop() to drop early).

struct Resource {
    name: String,
}

impl Resource {
    fn new(name: &str) -> Self {
        println!("acquiring: {name}");
        Resource { name: name.to_string() }
    }
}

impl Drop for Resource {
    fn drop(&mut self) {
        println!("releasing: {}", self.name);
    }
}

fn main() {
    let _a = Resource::new("database connection");
    let _b = Resource::new("file handle");
    println!("resources in use...");
    // Drop order is LIFO: _b dropped first, then _a
}

Output:

acquiring: database connection
acquiring: file handle
resources in use...
releasing: file handle
releasing: database connection

Dropping early

fn main() {
    let conn = Resource::new("db");
    println!("doing work...");

    drop(conn); // explicit early drop — calls Drop::drop()
    println!("conn already released");

    // conn.name; ← compile error: value was dropped
}

Drop in practice — RAII pattern

use std::sync::{Arc, Mutex};

struct DbPool {
    connections: Arc<Mutex<Vec<String>>>,
}

struct DbConnection {
    id: String,
    pool: Arc<Mutex<Vec<String>>>,
}

impl Drop for DbConnection {
    fn drop(&mut self) {
        // Return the connection back to the pool automatically
        self.pool.lock().unwrap().push(self.id.clone());
        println!("connection '{}' returned to pool", self.id);
    }
}

impl DbPool {
    fn new() -> Self {
        let conns = vec!["conn-1".into(), "conn-2".into(), "conn-3".into()];
        Self { connections: Arc::new(Mutex::new(conns)) }
    }

    fn get(&self) -> Option<DbConnection> {
        let id = self.connections.lock().unwrap().pop()?;
        println!("checked out: {id}");
        Some(DbConnection { id, pool: Arc::clone(&self.connections) })
    }
}

fn main() {
    let pool = DbPool::new();
    {
        let _c1 = pool.get(); // "conn-3"
        let _c2 = pool.get(); // "conn-2"
    } // both returned to pool here automatically
    println!("pool size: {}", pool.connections.lock().unwrap().len()); // 3
}

This is the RAII (Resource Acquisition Is Initialization) pattern — the same one Rust's MutexGuard, file handles, and socket wrappers use internally.


28. Custom Iterators

You can make any type iterable by implementing the Iterator trait, which requires only one method: next().

struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self { Self { count: 0, max } }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None // signals the iterator is exhausted
        }
    }
}

fn main() {
    let counter = Counter::new(5);

    // You get ALL iterator methods for free!
    let sum: u32 = counter.sum();
    println!("sum: {sum}"); // 15

    // Chain with other iterators
    let result: Vec<u32> = Counter::new(5)
        .zip(Counter::new(5).skip(1))   // pair consecutive values
        .map(|(a, b)| a * b)            // multiply each pair
        .filter(|x| x % 3 == 0)        // keep multiples of 3
        .collect();

    println!("{:?}", result); // [6, 12]
}

A real-world custom iterator — paginated results

struct Paginator {
    current_page: u32,
    total_pages: u32,
    page_size: u32,
}

impl Paginator {
    fn new(total_items: u32, page_size: u32) -> Self {
        Self {
            current_page: 0,
            total_pages: (total_items + page_size - 1) / page_size,
            page_size,
        }
    }
}

#[derive(Debug)]
struct Page {
    number: u32,
    offset: u32,
    limit: u32,
}

impl Iterator for Paginator {
    type Item = Page;

    fn next(&mut self) -> Option<Page> {
        if self.current_page < self.total_pages {
            self.current_page += 1;
            Some(Page {
                number: self.current_page,
                offset: (self.current_page - 1) * self.page_size,
                limit: self.page_size,
            })
        } else {
            None
        }
    }
}

fn main() {
    let pages = Paginator::new(95, 20);

    for page in pages {
        println!("Page {}: OFFSET {} LIMIT {}", page.number, page.offset, page.limit);
    }
}

Output:

Page 1: OFFSET 0 LIMIT 20
Page 2: OFFSET 20 LIMIT 20
Page 3: OFFSET 40 LIMIT 20
Page 4: OFFSET 60 LIMIT 20
Page 5: OFFSET 80 LIMIT 20

IntoIterator — making structs work in for loops

struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self { Self { items: vec![] } }
    fn push(&mut self, item: T) { self.items.push(item); }
}

impl<T> IntoIterator for Stack<T> {
    type Item = T;
    type IntoIter = std::vec::IntoIter<T>;

    fn into_iter(self) -> Self::IntoIter {
        self.items.into_iter()
    }
}

fn main() {
    let mut stack = Stack::new();
    stack.push(1);
    stack.push(2);
    stack.push(3);

    for item in stack { // works because of IntoIterator
        println!("{item}");
    }
}

29. Building a REST API with Axum

axum is the most popular async web framework in Rust, built on top of tokio and hyper. It uses Rust's type system to make routing and request extraction ergonomic and safe.

Setup

# Cargo.toml
[dependencies]
axum      = "0.7"
tokio     = { version = "1", features = ["full"] }
serde     = { version = "1", features = ["derive"] }
serde_json = "1"
uuid      = { version = "1", features = ["v4"] }

Full CRUD API — Todo List

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::Json,
    routing::{delete, get, post, put},
    Router,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use uuid::Uuid;

// --- Models ---

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Todo {
    id: String,
    title: String,
    completed: bool,
}

#[derive(Deserialize)]
struct CreateTodo {
    title: String,
}

#[derive(Deserialize)]
struct UpdateTodo {
    title: Option<String>,
    completed: Option<bool>,
}

// --- Shared state ---

type Db = Arc<Mutex<Vec<Todo>>>;

// --- Handlers ---

async fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> {
    let todos = db.lock().unwrap();
    Json(todos.clone())
}

async fn create_todo(
    State(db): State<Db>,
    Json(payload): Json<CreateTodo>,
) -> (StatusCode, Json<Todo>) {
    let todo = Todo {
        id: Uuid::new_v4().to_string(),
        title: payload.title,
        completed: false,
    };
    db.lock().unwrap().push(todo.clone());
    (StatusCode::CREATED, Json(todo))
}

async fn get_todo(
    State(db): State<Db>,
    Path(id): Path<String>,
) -> Result<Json<Todo>, StatusCode> {
    let todos = db.lock().unwrap();
    todos
        .iter()
        .find(|t| t.id == id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

async fn update_todo(
    State(db): State<Db>,
    Path(id): Path<String>,
    Json(payload): Json<UpdateTodo>,
) -> Result<Json<Todo>, StatusCode> {
    let mut todos = db.lock().unwrap();
    let todo = todos.iter_mut().find(|t| t.id == id).ok_or(StatusCode::NOT_FOUND)?;

    if let Some(title) = payload.title { todo.title = title; }
    if let Some(completed) = payload.completed { todo.completed = completed; }

    Ok(Json(todo.clone()))
}

async fn delete_todo(
    State(db): State<Db>,
    Path(id): Path<String>,
) -> StatusCode {
    let mut todos = db.lock().unwrap();
    let before = todos.len();
    todos.retain(|t| t.id != id);

    if todos.len() < before { StatusCode::NO_CONTENT }
    else { StatusCode::NOT_FOUND }
}

// --- Router ---

fn app(db: Db) -> Router {
    Router::new()
        .route("/todos",      get(list_todos).post(create_todo))
        .route("/todos/:id",  get(get_todo).put(update_todo).delete(delete_todo))
        .with_state(db)
}

// --- Main ---

#[tokio::main]
async fn main() {
    let db: Db = Arc::new(Mutex::new(vec![]));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("listening on http://localhost:3000");

    axum::serve(listener, app(db)).await.unwrap();
}

Test it with curl

# Create todos
curl -s -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Learn Rust"}' | jq

curl -s -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Build an API"}' | jq

# List all
curl -s http://localhost:3000/todos | jq

# Get one (replace ID)
curl -s http://localhost:3000/todos/<id> | jq

# Update
curl -s -X PUT http://localhost:3000/todos/<id> \
  -H "Content-Type: application/json" \
  -d '{"completed": true}' | jq

# Delete
curl -s -X DELETE http://localhost:3000/todos/<id>

Adding middleware — request logging

use axum::middleware::{self, Next};
use axum::extract::Request;
use axum::response::Response;

async fn log_request(req: Request, next: Next) -> Response {
    let method = req.method().clone();
    let uri    = req.uri().clone();
    let res    = next.run(req).await;
    println!("{} {} → {}", method, uri, res.status());
    res
}

fn app(db: Db) -> Router {
    Router::new()
        .route("/todos",     get(list_todos).post(create_todo))
        .route("/todos/:id", get(get_todo).put(update_todo).delete(delete_todo))
        .layer(middleware::from_fn(log_request)) // add logging to all routes
        .with_state(db)
}

Wrapping Up

Concept What it gives you
dyn Trait Runtime polymorphism, heterogeneous collections
Deref Transparent smart pointer ergonomics, auto-coercions
Drop RAII — deterministic cleanup, no GC needed
Custom iterators Any type can plug into Rust's iterator ecosystem
Axum Type-safe, async REST APIs on top of tokio

The full series

Part Topics
Part 1 Ownership, Borrowing, Lifetimes, Traits, Result/Option, Pattern Matching
Part 2 Closures, Iterators, Generics, Enums, Smart Pointers, Async/Await
Part 3 Macros, Modules, Cargo, Testing, Unsafe, FFI
Part 4 Threads, Channels, Send/Sync, Mutex, Atomics, clap
Part 5 dyn Trait, Deref, Drop, Custom Iterators, Axum REST API

What's in Part 6?

  • Workspaces and multi-crate projects
  • serde — serialization deep dive
  • Benchmarking with criterion
  • Error handling patterns with thiserror and anyhow
  • Writing a real-world CLI + library crate

Found this useful? Drop a ❤️ and follow for Part 6!

More Posts

Rust Concurrency: Threads, Channels, Mutex & Sync (Part 4)

mihir010 - May 27

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

Sovereign Intelligence: The Complete 25,000 Word Blueprint (Download)

Pocket Portfolio - Apr 1

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!