Idioms, Patterns & API Guidelines

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Knowing the syntax is not the same as writing code that other Rust programmers find natural. This page collects the idioms and patterns that experienced Rust code uses — largely from the community Rust Design Patterns book, which is labelled as community material — and the official Rust API Guidelines, which describe how public library APIs should look. Examples use the shelf catalogue.

Idioms

Accept borrowed types

Take &str rather than &String, &[T] rather than &Vec<T>, &Path (or impl AsRef<Path>) rather than &PathBuf. The borrowed form accepts more callers (literals, slices of arrays, sub-strings) at no cost, thanks to deref coercion.

fn title_case(title: &str) -> String {               // not &String
    title.split(' ').map(|w| {
        let mut chars = w.chars();
        chars.next().map(|c| c.to_uppercase().collect::<String>() + chars.as_str()).unwrap_or_default()
    }).collect::<Vec<_>>().join(" ")
}

fn total_pages(pages: &[u32]) -> u32 {                // not &Vec<u32>
    pages.iter().sum()
}

fn main() {
    let owned = String::from("the left hand of darkness");
    println!("{} | {}", title_case(&owned), title_case("dune messiah"));
    let pages: Vec<u32> = "412 180".split(' ').filter_map(|p| p.parse().ok()).collect();
    println!("{} {}", total_pages(&pages), total_pages(&[96; 3])); // &Vec<u32> and &[u32; 3] both coerce
}

Constructors and Default

Rust has no constructors in the language; the convention is an associated function new (plus with_*/from_* variants), and Default for types with a sensible empty value. Implement or derive Default whenever new() takes no arguments, so the type works with ..Default::default(), mem::take and unwrap_or_default.

mem::take and mem::replace to move out of a &mut

When you own only a &mut to a value but need to move one of its fields (to transform an enum state, say), swap a cheap placeholder in with mem::take (uses Default) or mem::replace, instead of cloning:

use std::mem;

enum Draft {
    Editing { text: String },
    Submitted { text: String, reviewer: String },
}

impl Draft {
    fn submit(&mut self, reviewer: &str) {
        if let Draft::Editing { text } = self {
            let text = mem::take(text);           // move the String out, leave "" behind
            *self = Draft::Submitted { text, reviewer: reviewer.into() };
        }
    }
}

fn main() {
    let mut d = Draft::Editing { text: "A great read.".into() };
    d.submit("librarian");
    if let Draft::Submitted { text, reviewer } = &d {
        println!("{reviewer}: {text}");
    }
}

Other idioms in brief

  • Option as an iterator: Option implements IntoIterator, so vec.extend(maybe_item) or chain(opt) handle the Some/None cases without a branch.

  • Temporary mutability: build data with a mut binding, then shadow it immutably (let data = data;) or build it in a block expression.

  • Return the consumed argument on error: a function that takes ownership and may fail returns the value inside the error (Errvalue, reason), so callers can retry without having cloned it — as String::from_utf8 does with FromUtf8Error::into_bytes.

  • Finalisation in destructors: use RAII guards instead of finally (see Drop, RAII & Drop Check).

  • Concatenate strings with format! when it is clearer than repeated push_str.

Design patterns

Builder

Rust has no overloading or default arguments, so types with many optional settings get a builder: a separate type with chained setters and a final build() that validates. Builders can take self by value (chaining moves the builder) or &mut self (reusable); by-value is the more common choice.

#[derive(Debug)]
pub struct SearchQuery {
    text: String,
    limit: usize,
    include_lent: bool,
    sort_by_year: bool,
}

#[derive(Default)]
pub struct SearchQueryBuilder {
    text: Option<String>,
    limit: Option<usize>,
    include_lent: bool,
    sort_by_year: bool,
}

impl SearchQueryBuilder {
    pub fn text(mut self, text: impl Into<String>) -> Self {
        self.text = Some(text.into());
        self
    }
    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = Some(limit);
        self
    }
    pub fn include_lent(mut self, yes: bool) -> Self {
        self.include_lent = yes;
        self
    }
    pub fn sort_by_year(mut self) -> Self {
        self.sort_by_year = true;
        self
    }
    pub fn build(self) -> Result<SearchQuery, String> {
        let text = self.text.filter(|t| !t.trim().is_empty()).ok_or("text is required")?;
        let limit = self.limit.unwrap_or(20);
        if limit == 0 || limit > 100 {
            return Err(format!("limit must be 1-100, got {limit}"));
        }
        Ok(SearchQuery { text, limit, include_lent: self.include_lent, sort_by_year: self.sort_by_year })
    }
}

impl SearchQuery {
    pub fn builder() -> SearchQueryBuilder {
        SearchQueryBuilder::default()
    }
}

fn main() {
    let q = SearchQuery::builder().text("dune").limit(10).sort_by_year().build();
    println!("{q:?}");
    println!("{:?}", SearchQuery::builder().limit(500).text("x").build());
}

For many builders, a derive macro (derive_builder, bon, or the small one on Macros) removes the boilerplate. When illegal combinations should be compile errors rather than build() errors, combine the builder with typestate.

Newtype

Wrap a type in a single-field tuple struct to give it a distinct type, attach invariants and methods, hide its representation, or implement foreign traits for it. shelf-core's Isbn(String) guarantees a valid check digit; a MemberId(u32) cannot be passed where a BookId(u32) is expected. See Structs & Methods.

RAII guards

Tie an action to a value’s lifetime: acquire in the constructor, release in Drop. MutexGuard, File, database transactions and the Rollback guard on Drop, RAII & Drop Check are all instances.

Strategy with traits or closures

Varying an algorithm is done with a trait (static dispatch through generics, or Box<dyn Trait> when chosen at run time) or simply with a closure parameter — often the lightest option:

trait FeePolicy {
    fn fee_cents(&self, days_late: u32) -> u32;
}

struct Standard;
struct Student;

impl FeePolicy for Standard {
    fn fee_cents(&self, days_late: u32) -> u32 {
        days_late.min(10) * 50
    }
}
impl FeePolicy for Student {
    fn fee_cents(&self, days_late: u32) -> u32 {
        days_late.saturating_sub(2).min(10) * 25
    }
}

fn invoice(policy: &dyn FeePolicy, days: u32) -> u32 {          // strategy chosen at run time
    policy.fee_cents(days)
}

fn invoice_with(days: u32, fee: impl Fn(u32) -> u32) -> u32 {    // strategy as a closure
    fee(days)
}

fn main() {
    let policies: [(&str, Box<dyn FeePolicy>); 2] = [("standard", Box::new(Standard)), ("student", Box::new(Student))];
    for (name, p) in &policies {
        println!("{name}: {}", invoice(p.as_ref(), 5));
    }
    println!("holiday: {}", invoice_with(5, |_| 0));
}

Typestate

Encode an object’s state in its type parameter so that invalid transitions do not compile (ImportJob<Configured> has no run; ImportJob<Validated> does). Shown on Generics & Const Generics.

Command

Represent operations as values — usually an enum — so they can be queued, logged, undone or sent to another task. In Rust the enum form is idiomatic, and an actor task that owns state and processes a Command channel is the async version (see Async Runtimes & Tokio):

#[derive(Debug, Clone)]
enum Command {
    Lend { isbn: String, member: String },
    Return { isbn: String },
}

#[derive(Default)]
struct Desk {
    lent: Vec<(String, String)>,
    history: Vec<Command>,
}

impl Desk {
    fn execute(&mut self, cmd: Command) {
        match &cmd {
            Command::Lend { isbn, member } => self.lent.push((isbn.clone(), member.clone())),
            Command::Return { isbn } => self.lent.retain(|(i, _)| i != isbn),
        }
        self.history.push(cmd); // commands are data: easy to log, replay or undo
    }
}

fn main() {
    let mut desk = Desk::default();
    desk.execute(Command::Lend { isbn: "978".into(), member: "alice".into() });
    desk.execute(Command::Return { isbn: "978".into() });
    println!("{} on loan, {} commands logged", desk.lent.len(), desk.history.len());
}

Visitor

For operations over a tree of heterogeneous nodes (an AST, a document), a visitor trait with one method per node kind separates traversal from the operation. With a closed set of node types, a match over an enum is usually simpler; the visitor pattern shines when users of a library add operations (serde’s Visitor is a well-known example).

Anti-patterns

  • Cloning to satisfy the borrow checker. Sprinkling .clone() until errors disappear hides an ownership design problem and costs allocations. Restructure: shorten borrows, split structs, pass indices, or share with Rc/Arc deliberately. (Cloning is fine when you genuinely need a second copy, and in prototypes.)

  • #![deny(warnings)] in source. A new compiler release can add warnings and break the build for everyone downstream; deny warnings in CI with -D warnings instead (see Tooling).

  • Deref polymorphism. Implementing Deref on a struct to "inherit" another type’s methods emulates inheritance badly: methods appear implicitly, traits are not inherited, and readers are surprised. Delegate explicitly or use AsRef.

The Rust API Guidelines

The official Rust API Guidelines are a checklist for public library APIs, grouped by theme. The highlights:

Theme Selected guidelines

Naming (C-CASE, C-CONV, C-GETTER, C-ITER)

RFC 430 casing; as_/to_/into_ conversion prefixes; no get_ prefix on getters; iter/iter_mut/into_iter

Interoperability (C-COMMON-TRAITS, C-CONV-TRAITS, C-SEND-SYNC, C-GOOD-ERR)

eagerly implement Debug, Clone, Eq, Hash, Default…​; conversions via From/AsRef; types are Send/Sync where possible; meaningful error types

Documentation (C-CRATE-DOC, C-EXAMPLE, C-FAILURE)

crate-level docs with examples; every item has an example; document errors, panics and safety

Predictability (C-CTOR, C-METHOD, C-SMART-PTR)

constructors are static inherent methods; functions with a clear receiver are methods; smart pointers do not add inherent methods

Flexibility (C-CALLER-CONTROL, C-GENERIC, C-OBJECT)

let callers decide where to copy and allocate; minimise assumptions with generics; traits are object-safe (dyn-compatible) if they may be used as trait objects

Type safety (C-NEWTYPE, C-CUSTOM-TYPE, C-BITFLAG, C-BUILDER)

newtypes for static distinctions; custom types instead of bool arguments; builders for complex construction

Dependability (C-VALIDATE, C-DTOR-FAIL, C-DTOR-BLOCK)

validate arguments; destructors never fail and do not block

Future proofing (C-SEALED, C-STRUCT-PRIVATE, C-STRUCT-BOUNDS)

sealed traits; private fields; no trait bounds on struct definitions that would leak into every user

A small example of "custom types instead of bool arguments": catalog.search(query, true, false) tells the reader nothing; catalog.search(query, Availability::IncludeLent, Order::ByTitle) does.

Rust for object-oriented developers

The Book’s chapter 18 asks whether Rust is object-oriented. The mapping for developers coming from Java, C# or C++:

OOP concept Rust equivalent

class with fields and methods

struct (or enum) + impl block

encapsulation

module privacy: private fields, public methods

interface

trait

inheritance of behaviour

default methods in traits; composition and delegation

inheritance of data

composition: embed a struct as a field

polymorphism

generics with trait bounds (static) or dyn Trait (dynamic)

abstract class

trait with required and default methods

constructor / destructor

new associated function / Drop

null object

Option<T>

exceptions

Result<T, E> and ?; panic! for bugs

singleton

pass state explicitly; OnceLock/LazyLock if a global is truly needed

The Book implements its blog-post workflow twice — with trait-object states (the classic State pattern) and with distinct types per state (typestate) — and concludes that the second, which makes invalid states unrepresentable, plays to Rust’s strengths.