# Rust API Design Patterns Patterns for designing public APIs in Rust, extracted from the standard library source. **Source:** [rust-lang/rust](https://github.com/rust-lang/rust) at commit [`f53b654`](https://github.com/rust-lang/rust/tree/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6) **Stats:** 18,430 pub fn, 919 pub(crate), 53 #[non_exhaustive], 889 is_/as_/to_/into_ methods, 162 new/build constructors, 2,676 impl blocks. --- ## 1. Naming Conventions (is_, as_, to_, into_) ### Source: 889 methods following these naming conventions in library/. ```rust impl Option { pub fn is_some(&self) -> bool { ... } // is_ → bool check pub fn as_ref(&self) -> Option<&T> { ... } // as_ → cheap ref conversion } impl String { pub fn as_str(&self) -> &str { ... } // as_ → borrow (no allocation) pub fn into_bytes(self) -> Vec { ... } // into_ → consuming conversion } impl Path { pub fn to_str(&self) -> Option<&str> { ... } // to_ → potentially expensive pub fn to_string_lossy(&self) -> Cow { ... } // to_ → may allocate } ``` ### Why The prefix tells you the cost and ownership semantics without reading the implementation: - `is_` → Returns bool, no allocation, &self - `as_` → Cheap conversion, returns a reference, &self - `to_` → Potentially expensive, may allocate, &self - `into_` → Consumes self, zero-cost ownership transfer ### When to Use **Triggers:** - `is_`: Predicate (yes/no check on &self) - `as_`: Cheap view (reinterpret bits, return &T) - `to_`: Conversion that may allocate or compute (returns owned) - `into_`: Consuming conversion (self → OtherType) **Example — before:** ```rust impl Email { fn get_domain(&self) -> &str { ... } // "get" prefix — Java style fn make_lowercase(self) -> Email { ... } // "make" — unclear cost fn check_valid(&self) -> bool { ... } // "check" — inconsistent } ``` **Example — after:** ```rust impl Email { fn as_domain(&self) -> &str { ... } // as_ → cheap, returns ref fn into_lowercase(self) -> Email { ... } // into_ → consumes self fn is_valid(&self) -> bool { ... } // is_ → predicate fn to_ascii(&self) -> String { ... } // to_ → allocates new String } ``` ### When NOT to Use **Don't use this when:** - The method doesn't fit any prefix (use a descriptive verb) - `new` for constructors (not `create_` or `make_`) - `get`/`set` for simple field access on non-collection types --- ## 2. #[non_exhaustive] for Future-Proof Enums ### Source: [library/std/src/io/error.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/io/error.rs) (ErrorKind) 53 `#[non_exhaustive]` annotations in the stdlib. ```rust #[non_exhaustive] pub enum ErrorKind { NotFound, PermissionDenied, ConnectionRefused, // ... can add more variants without breaking downstream } ``` ### Why Without `#[non_exhaustive]`, adding an enum variant is a breaking change (downstream `match` statements become non-exhaustive). With it, match statements require a wildcard arm `_ =>`, so you can add variants freely in minor versions. ### When to Use **Triggers:** - Public enums in libraries that may grow - Error kind enums (new error types are common) - Status/state enums that evolve over time - Struct fields you might add to later **Example — before:** ```rust pub enum Color { Red, Green, Blue } // Later, adding Yellow breaks ALL downstream match statements: // error: non-exhaustive patterns: `Yellow` not covered ``` **Example — after:** ```rust #[non_exhaustive] pub enum Color { Red, Green, Blue } // Downstream must have wildcard: match color { Color::Red => "red", Color::Green => "green", Color::Blue => "blue", _ => "unknown", // required — handles future variants } // Adding Color::Yellow in next release is non-breaking! ``` ### When NOT to Use **Don't use this when:** - The enum is closed (boolean-like: On/Off) - Internal use only (pub(crate)) - You WANT exhaustive matching for safety (Option, Result) --- ## 3. pub(crate) for Internal Visibility ### Source: 919 `pub(crate)` usages in the stdlib. ```rust pub(crate) fn internal_helper() { ... } pub(crate) struct InternalState { ... } ``` ### Why `pub(crate)` makes items visible within the crate but not to external users. It's the middle ground between `pub` (everyone) and private (only the module). Keeps internal architecture flexible without exposing implementation details. ### When to Use **Triggers:** - Helper functions used across modules but not part of the public API - Internal types that multiple modules need but users shouldn't see - Test utilities that need cross-module access **Example — before:** ```rust // Private — can't use from sibling modules fn helper() { ... } // Public — now it's part of your API contract forever pub fn helper() { ... } ``` **Example — after:** ```rust // Visible to the whole crate, invisible to users pub(crate) fn helper() { ... } ``` ### When NOT to Use **Don't use this when:** - The item is only used in one module (just make it private) - The item should be part of the public API (use `pub`) - You're in a binary crate (there are no external users anyway) --- ## 4. Constructor Pattern (fn new) ### Source: 162 `new`/`build` methods in the stdlib. ```rust impl Vec { pub const fn new() -> Self { Vec { ptr: NonNull::dangling(), len: 0, cap: 0 } } pub fn with_capacity(capacity: usize) -> Self { ... } } ``` ### Why `new()` is the standard constructor name in Rust (not `create`, `init`, `make`). Parameterized constructors use `with_` prefix or a builder pattern. `new()` should be cheap and infallible when possible. ### When to Use **Triggers:** - `new()` — default or minimal construction - `with_capacity()` — pre-allocating variant - `from_*()` — construction from specific inputs (not From trait) - Builder pattern — many optional parameters **Example — before:** ```rust impl Server { pub fn create(host: &str, port: u16, threads: usize, tls: bool) -> Self { ... } // 4 positional parameters — easy to mix up port and threads } ``` **Example — after:** ```rust impl Server { pub fn new(addr: SocketAddr) -> Self { ... } // minimal required args pub fn builder() -> ServerBuilder { ... } // for complex configuration } impl ServerBuilder { pub fn threads(mut self, n: usize) -> Self { self.threads = n; self } pub fn tls(mut self, enabled: bool) -> Self { self.tls = enabled; self } pub fn build(self) -> Result { ... } } ``` ### When NOT to Use **Don't use this when:** - Construction can fail (use `try_new()` → Result, or builder with fallible `build()`) - Multiple constructors exist (use descriptive names: `from_path`, `from_reader`) --- ## 5. Method Chains (Consuming Self → Self) ### Source: Iterator adapters are the definitive example of method chaining. ```rust let result: Vec = vec![1, 2, 3, 4, 5] .into_iter() .filter(|x| x % 2 == 0) .map(|x| x * x) .collect(); ``` ### Why Method chaining creates fluent, readable APIs. Each method consumes self and returns a new (or modified) Self. The ownership model makes this natural — no mutation through shared references. ### When to Use **Triggers:** - Builder patterns (each step returns modified builder) - Iterator adapters (each step wraps the previous) - Configuration objects **Example — before:** ```rust let mut query = Query::new("users"); query.set_filter("age > 18"); query.set_limit(10); query.set_order("name"); let results = query.execute()?; ``` **Example — after:** ```rust let results = Query::new("users") .filter("age > 18") .limit(10) .order_by("name") .execute()?; ``` ### When NOT to Use **Don't use this when:** - Operations can fail mid-chain (errors get awkward) - The object needs to be reused after each call - It makes the code LESS readable (very long chains) --- ## 6. Generic Parameters with Trait Bounds ### Source: ```rust // Accept anything that can be referenced as a Path: pub fn read>(path: P) -> io::Result> { ... } // Accept anything comparable: pub fn max(a: T, b: T) -> T { ... } // Multiple bounds: pub fn sort_and_print(items: &mut [T]) { ... } ``` ### Why Generics with trait bounds give you maximum flexibility while maintaining type safety. The function works with ANY type that satisfies the bounds — but the compiler still checks everything at compile time. ### When to Use **Triggers:** - Function should work with multiple concrete types - You want to accept user-defined types that implement your trait - Performance (monomorphization — no virtual dispatch) **Example — before:** ```rust // Only works with String — can't pass &str, PathBuf, etc. fn log_to_file(path: String, message: String) { ... } ``` **Example — after:** ```rust // Works with any type that can cheaply become &Path and &str fn log_to_file(path: impl AsRef, message: impl AsRef) { let path = path.as_ref(); let message = message.as_ref(); // ... } // Now accepts: "file.txt", String::from("file.txt"), PathBuf, etc. ``` ### When NOT to Use **Don't use this when:** - Only one type will ever be used (concrete type is simpler) - You need dynamic dispatch (use `dyn Trait` instead) - The generic makes the signature unreadable (too many bounds) --- ## 7. Typestate Pattern (Compile-Time State Machines) ### Source: The stdlib uses this in `File` (open modes) and throughout. The pattern encodes state in the type system. ```rust // Different types for different states — impossible to misuse struct Connection { inner: TcpStream, _state: PhantomData } struct Connecting; struct Connected; struct Authenticated; impl Connection { fn connect(addr: &str) -> Result, Error> { ... } } impl Connection { fn authenticate(self, creds: &Creds) -> Result, Error> { ... } } impl Connection { fn query(&self, sql: &str) -> Result { ... } } // Can't call query() without authenticating first — compile error! ``` ### Why The type system prevents invalid state transitions. You literally cannot call methods in the wrong order because they don't exist on that type state. Bugs become compile errors. ### When to Use **Triggers:** - State machine with ordered transitions - Protocol sequences (connect → auth → use) - Builder with required steps ### When NOT to Use **Don't use this when:** - States are dynamic (determined at runtime) - There are too many states (exponential type explosion) - The API becomes confusing (type parameters everywhere) --- ## 8. Extension Traits for Adding Methods ### Source: ```rust // std adds methods to Iterator via extension traits: pub trait IteratorExt: Iterator + Sized { fn take_while_ref

(&mut self, pred: P) -> TakeWhileRef { ... } } impl IteratorExt for I {} ``` ### Why Extension traits add methods to types you don't own (from another crate) without modifying the original. Users must import the extension trait to see the methods. ### When to Use **Triggers:** - Adding methods to external types (from other crates) - Optional functionality (only available when you import the trait) - Platform-specific extensions (e.g., `std::os::unix::fs::PermissionsExt`) ### When NOT to Use **Don't use this when:** - You own the type (just add methods directly) - A free function would be clearer - The extension is confusing (users won't find it via autocomplete) --- ## 9. Newtype Pattern for Type Safety ### Source: ```rust // Newtype: single-field tuple struct pub struct Instant(time::Instant); pub struct Duration { secs: u64, nanos: Nanoseconds } // Newtypes prevent mixing up semantically different values: struct Meters(f64); struct Seconds(f64); // Can't accidentally pass Meters where Seconds is expected ``` ### Why Newtypes create distinct types with zero runtime cost (same representation). They prevent confusing values that have the same underlying type but different semantics. ### When to Use **Triggers:** - Multiple parameters of the same primitive type (easy to swap) - Domain concepts that should be distinct (UserID vs PostID) - Adding methods or trait impls to foreign types **Example — before:** ```rust fn schedule_meeting(room_id: u64, user_id: u64, duration_mins: u64) { ... } // Easy to accidentally swap room_id and user_id — both are u64! schedule_meeting(user_id, room_id, 60); // compiles fine — but wrong ``` **Example — after:** ```rust struct RoomId(u64); struct UserId(u64); struct Minutes(u64); fn schedule_meeting(room: RoomId, user: UserId, duration: Minutes) { ... } // schedule_meeting(user_id, room_id, Minutes(60)); // COMPILE ERROR ``` ### When NOT to Use **Don't use this when:** - Only one parameter of that type exists (no confusion possible) - The wrapper adds no safety (just wrapping for the sake of it) - The ergonomic cost (`.0` access, impl forwarding) outweighs benefit --- ## 10. Error Type Design (Per-Module Errors) ### Source: The stdlib provides one error type per module: `io::Error`, `fmt::Error`, `num::ParseIntError`, `str::Utf8Error`. ```rust // Each module has its own error type: pub mod io { pub struct Error { repr: Repr } pub type Result = std::result::Result; } pub mod num { pub struct ParseIntError { kind: IntErrorKind } } ``` ### Why Per-module errors keep error types focused. `io::Error` handles all I/O errors with a kind enum + optional payload. This is more maintainable than one giant application error enum. ### When to Use **Triggers:** - Your module is self-contained with its own failure modes - Different modules have different error semantics - You want to provide `type Result` convenience alias ### When NOT to Use **Don't use this when:** - The module is tiny (just use the parent's error type) - Errors need to compose across modules (consider `thiserror`/`anyhow`) --- ## Summary: API Design Decision Tree ``` Naming a method? ├── Returns bool? → is_* ├── Returns &T (cheap)? → as_* ├── Returns T (may allocate)? → to_* ├── Consumes self → T? → into_* ├── Constructor? → new / with_* / from_* └── Other? → descriptive_verb Visibility? ├── External API → pub ├── Cross-module internal → pub(crate) └── Module-only → private (default) Enum might grow? └── YES → #[non_exhaustive] Many parameters? └── YES → Builder pattern (fn builder() → Builder) Accept multiple types? ├── Cheap reference → impl AsRef ├── Ownership transfer → impl Into └── Multiple bounds → T: Bound1 + Bound2 Prevent misuse? ├── Wrong argument order → Newtype pattern ├── Wrong method order → Typestate pattern └── Future compatibility → #[non_exhaustive] ``` | Pattern | Use when | |---|---| | `is_`/`as_`/`to_`/`into_` | Standard naming convention | | `#[non_exhaustive]` | Enum/struct may gain variants/fields | | `pub(crate)` | Internal cross-module access | | `fn new` / builder | Construction | | Method chaining | Fluent APIs (config, queries, iterators) | | Trait bounds | Generic functions accepting multiple types | | Typestate | Compile-time state machine enforcement | | Extension traits | Adding methods to foreign types | | Newtype | Type-safe wrappers around primitives | | Per-module errors | Self-contained error types per module | See also: - [traits.md](traits.md) — Trait design patterns - [error-handling.md](error-handling.md) — Error type design - [documentation.md](documentation.md) — Documenting public APIs