API Design
Core Question
What does the caller need, and what should the compiler prevent?
Every API decision flows from this:
- What is the minimum the caller must provide?
- What mistakes can the type system catch at compile time?
- What is the cost of each operation, and does the name
communicate it?
If the caller can misuse your API without a compiler error,
the API needs work.
API → Design Question
| Symptom |
Don't Just Say |
Ask Instead |
| Constructor with 8 parameters |
"Use a builder" |
Which parameters are required vs optional? |
| Caller ignores return value |
"Add must_use" |
Is ignoring this value ever correct? |
| Adding enum variant breaks users |
"It's a breaking change" |
Should this enum be #[non_exhaustive]? |
Method named get_name() |
"Remove the get_" |
Does this do more than return a field? |
as_string() allocates |
"Rename to to_string()" |
What is the actual cost of this conversion? |
Quick Decisions
| Scenario |
Use |
Why |
| Many optional fields in constructor |
Builder pattern |
Self-documenting, flexible |
| Required + optional fields |
Typestate builder |
Compiler enforces required fields |
| All fields have sensible defaults |
#[derive(Default)] |
Works with ..Default::default() |
| Return value must not be ignored |
#[must_use] |
Compiler warns on silent drop |
| Builder struct or method chain |
#[must_use] on type + methods |
Prevents accidental drop |
| Public enum that may grow |
#[non_exhaustive] |
Add variants without breaking change |
| Public struct that may grow |
#[non_exhaustive] + constructor |
Add fields without breaking change |
| Adding methods to external types |
Extension trait (TypeExt) |
Works around orphan rules |
| Public type minimum traits |
Debug, Clone, PartialEq |
Basic ecosystem interop |
| Serde in a library crate |
Feature flag, not hard dep |
Users who don't need it don't pay |
| Free reference conversion |
as_ prefix |
Signals O(1), no allocation |
| Allocating conversion |
to_ prefix |
Signals cost |
| Ownership-consuming conversion |
into_ prefix |
Signals self is consumed |
| Simple field accessor |
No get_ prefix |
name() not get_name() |
| Boolean-returning method |
is_/has_/can_ prefix |
Reads naturally in conditions |
Builder Pattern
Choose the right builder variant based on your
requirements:
Decision
| Variant |
When |
build() returns |
| Infallible |
All fields have defaults |
T |
| Fallible |
Validation can fail at runtime |
Result<T, E> |
| Typestate |
Required fields enforced at compile time |
T |
Consuming (mut self) |
Most common, simple chaining |
Depends |
Borrowing (&mut self) |
Builder reused for multiple instances |
Depends |
Infallible Builder
#[derive(Default)]
#[must_use = "builders do nothing unless you call build()"]
pub struct WidgetBuilder {
color: Option<Color>,
size: Option<Size>,
}
impl WidgetBuilder {
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn build(self) -> Widget {
Widget {
color: self.color.unwrap_or(Color::Black),
size: self.size.unwrap_or(Size::Medium),
}
}
}
Typestate Builder (compile-time required fields)
pub struct NoUrl;
pub struct HasUrl(String);
pub struct ClientBuilder<Url> {
url: Url,
timeout: Option<Duration>,
}
impl ClientBuilder<NoUrl> {
pub fn new() -> Self {
Self { url: NoUrl, timeout: None }
}
pub fn url(self, url: String) -> ClientBuilder<HasUrl> {
ClientBuilder { url: HasUrl(url), timeout: self.timeout }
}
}
impl ClientBuilder<HasUrl> {
pub fn build(self) -> Client {
Client { url: self.url.0, timeout: self.timeout }
}
}
Common Traits Checklist
Derive these for every public type unless you have a
reason not to:
| Type Category |
Derive |
| Minimum (all public types) |
Debug, Clone, PartialEq |
| ID / key types |
Debug, Clone, Copy, PartialEq, Eq, Hash |
| Small value types |
Debug, Clone, Copy, PartialEq, Default |
| Config / options |
Debug, Clone, PartialEq, Default |
| Error types |
Debug, Clone, PartialEq, Eq |
| HashMap keys |
Add Eq, Hash |
| BTreeMap keys |
Add Eq, Ord, PartialOrd |
| Serde support |
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] |
Implement manually when derive does the wrong thing
(e.g., case-insensitive equality, redacting sensitive
fields in Debug).
Naming Quick Reference
Conversion Prefixes
| Prefix |
Cost |
Ownership |
Example |
as_ |
Free O(1) |
&self -> &U |
as_str(), as_bytes(), as_slice() |
to_ |
Allocates/computes |
&self -> U |
to_string(), to_vec(), to_lowercase() |
into_ |
Consumes self |
self -> U |
into_inner(), into_bytes(), into_vec() |
Accessor Naming
| Pattern |
Name |
Not |
| Simple field access |
name(), len() |
get_name(), get_len() |
| Fallible lookup |
get(), get_mut() |
find() (unless searching) |
| Boolean check |
is_empty(), has_key(), can_write() |
empty(), key_exists() |
| Setter |
set_name(value) |
name(value) (unless builder) |
Iterator Methods
| Method |
Yields |
Ownership |
iter() |
&T |
Borrows collection |
iter_mut() |
&mut T |
Mutably borrows |
into_iter() |
T |
Consumes collection |
Iterator type names match their method: iter() ->
Iter, into_iter() -> IntoIter, keys() -> Keys.
General Rules
| Element |
Convention |
Example |
Not |
| Types, traits, enums |
UpperCamelCase |
HttpServer |
HTTPServer |
| Enum variants |
UpperCamelCase |
NotFound |
NOT_FOUND |
| Functions, methods |
snake_case |
parse_json() |
parseJSON() |
| Constants, statics |
SCREAMING_SNAKE_CASE |
MAX_RETRIES |
maxRetries |
| Lifetimes |
Short lowercase |
'a, 'de, 'src |
'input_lifetime |
| Type params |
Single uppercase |
T, E, K, V |
ElementType |
| Acronyms |
Treat as words |
Uuid, HttpClient |
UUID, HTTPClient |
| Crate names |
No -rs/-rust suffix |
json-parser |
json-parser-rs |
Usage Scenarios
Scenario 1: Designing a Library Config Type
You need a Config struct with 3 required fields and 5
optional fields.
- Use a builder with typestate for the 3 required fields
- Add
#[must_use] to the builder type
- Derive
Debug, Clone, PartialEq, Default on Config
- Add
#[non_exhaustive] if the struct is public and may
gain fields
- Gate serde behind a feature flag
- Document with
# Examples showing builder usage
Scenario 2: Adding Conversion Methods to a Newtype
You have struct Email(String):
impl Email {
/// Returns the email as a string slice. O(1), no allocation.
pub fn as_str(&self) -> &str {
&self.0
}
/// Returns a new lowercase version. Allocates.
pub fn to_lowercase(&self) -> Email {
Email(self.0.to_lowercase())
}
/// Consumes the Email, returning the inner String.
pub fn into_string(self) -> String {
self.0
}
pub fn is_valid(&self) -> bool {
self.0.contains('@')
}
}
Scenario 3: Extending an External Type
You need hex encoding for byte slices:
pub trait ByteSliceExt {
fn as_hex(&self) -> String;
}
impl ByteSliceExt for [u8] {
fn as_hex(&self) -> String {
self.iter().map(|b| format!("{b:02x}")).collect()
}
}
Import use my_crate::ByteSliceExt; to use. Name the
trait with Ext suffix.
Reference Index
| Reference |
Read When |
| api-patterns |
Implementing builders, choosing #[must_use]/#[non_exhaustive], extension traits, Default, common traits, serde gating |
| api-naming |
Naming methods, types, conversions, iterators, or crate names |
| api-documentation |
Writing doc comments, examples, error/panic/safety sections, intra-doc links, Cargo.toml metadata |
Cross-References
| Need |
Skill |
| Error types for Result-returning APIs |
rust-errors |
| Newtype patterns, typestate, PhantomData |
rust-types |
| Trait design, generics, dispatch |
rust-types |
| Testing doc examples |
rust-quality |
| Clippy lints for API quality |
rust-quality |
| Ownership decisions in API signatures |
rust-ownership |
1---2name: rust-api3description: Rust API design, naming conventions, and documentation standards. Use when designing public APIs, implementing builder patterns, choosing #[must_use]/#[non_exhaustive], following Rust naming conventions (as_/to_/into_ prefixes), or writing doc comments with examples. Also use for library design decisions like common trait implementations and serde feature gating.4---56# API Design78## Core Question910**What does the caller need, and what should the compiler prevent?**1112Every API decision flows from this:1314- What is the minimum the caller must provide?15- What mistakes can the type system catch at compile time?16- What is the cost of each operation, and does the name17 communicate it?1819If the caller can misuse your API without a compiler error,20the API needs work.2122---2324## API → Design Question2526| Symptom | Don't Just Say | Ask Instead |27| -------------------------------- | ------------------------ | ------------------------------------------- |28| Constructor with 8 parameters | "Use a builder" | Which parameters are required vs optional? |29| Caller ignores return value | "Add must_use" | Is ignoring this value ever correct? |30| Adding enum variant breaks users | "It's a breaking change" | Should this enum be `#[non_exhaustive]`? |31| Method named `get_name()` | "Remove the get\_" | Does this do more than return a field? |32| `as_string()` allocates | "Rename to to_string()" | What is the actual cost of this conversion? |3334---3536## Quick Decisions3738| Scenario | Use | Why |39| ----------------------------------- | --------------------------------- | ------------------------------------ |40| Many optional fields in constructor | Builder pattern | Self-documenting, flexible |41| Required + optional fields | Typestate builder | Compiler enforces required fields |42| All fields have sensible defaults | `#[derive(Default)]` | Works with `..Default::default()` |43| Return value must not be ignored | `#[must_use]` | Compiler warns on silent drop |44| Builder struct or method chain | `#[must_use]` on type + methods | Prevents accidental drop |45| Public enum that may grow | `#[non_exhaustive]` | Add variants without breaking change |46| Public struct that may grow | `#[non_exhaustive]` + constructor | Add fields without breaking change |47| Adding methods to external types | Extension trait (`TypeExt`) | Works around orphan rules |48| Public type minimum traits | `Debug, Clone, PartialEq` | Basic ecosystem interop |49| Serde in a library crate | Feature flag, not hard dep | Users who don't need it don't pay |50| Free reference conversion | `as_` prefix | Signals O(1), no allocation |51| Allocating conversion | `to_` prefix | Signals cost |52| Ownership-consuming conversion | `into_` prefix | Signals self is consumed |53| Simple field accessor | No `get_` prefix | `name()` not `get_name()` |54| Boolean-returning method | `is_`/`has_`/`can_` prefix | Reads naturally in conditions |5556---5758## Builder Pattern5960Choose the right builder variant based on your61requirements:6263### Decision6465| Variant | When | `build()` returns |66| --------------------------- | ---------------------------------------- | ----------------- |67| **Infallible** | All fields have defaults | `T` |68| **Fallible** | Validation can fail at runtime | `Result<T, E>` |69| **Typestate** | Required fields enforced at compile time | `T` |70| **Consuming** (`mut self`) | Most common, simple chaining | Depends |71| **Borrowing** (`&mut self`) | Builder reused for multiple instances | Depends |7273### Infallible Builder7475```rust76#[derive(Default)]77#[must_use = "builders do nothing unless you call build()"]78pub struct WidgetBuilder {79 color: Option<Color>,80 size: Option<Size>,81}8283impl WidgetBuilder {84 pub fn color(mut self, color: Color) -> Self {85 self.color = Some(color);86 self87 }8889 pub fn build(self) -> Widget {90 Widget {91 color: self.color.unwrap_or(Color::Black),92 size: self.size.unwrap_or(Size::Medium),93 }94 }95}96```9798### Typestate Builder (compile-time required fields)99100```rust101pub struct NoUrl;102pub struct HasUrl(String);103104pub struct ClientBuilder<Url> {105 url: Url,106 timeout: Option<Duration>,107}108109impl ClientBuilder<NoUrl> {110 pub fn new() -> Self {111 Self { url: NoUrl, timeout: None }112 }113114 pub fn url(self, url: String) -> ClientBuilder<HasUrl> {115 ClientBuilder { url: HasUrl(url), timeout: self.timeout }116 }117}118119impl ClientBuilder<HasUrl> {120 pub fn build(self) -> Client {121 Client { url: self.url.0, timeout: self.timeout }122 }123}124```125126---127128## Common Traits Checklist129130Derive these for every public type unless you have a131reason not to:132133| Type Category | Derive |134| ------------------------------ | ---------------------------------------------------------------- |135| **Minimum (all public types)** | `Debug, Clone, PartialEq` |136| **ID / key types** | `Debug, Clone, Copy, PartialEq, Eq, Hash` |137| **Small value types** | `Debug, Clone, Copy, PartialEq, Default` |138| **Config / options** | `Debug, Clone, PartialEq, Default` |139| **Error types** | `Debug, Clone, PartialEq, Eq` |140| **HashMap keys** | Add `Eq, Hash` |141| **BTreeMap keys** | Add `Eq, Ord, PartialOrd` |142| **Serde support** | `#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]` |143144Implement manually when derive does the wrong thing145(e.g., case-insensitive equality, redacting sensitive146fields in Debug).147148---149150## Naming Quick Reference151152### Conversion Prefixes153154| Prefix | Cost | Ownership | Example |155| ------- | ------------------ | ------------- | -------------------------------------------- |156| `as_` | Free O(1) | `&self -> &U` | `as_str()`, `as_bytes()`, `as_slice()` |157| `to_` | Allocates/computes | `&self -> U` | `to_string()`, `to_vec()`, `to_lowercase()` |158| `into_` | Consumes self | `self -> U` | `into_inner()`, `into_bytes()`, `into_vec()` |159160### Accessor Naming161162| Pattern | Name | Not |163| ------------------- | ---------------------------------------- | ------------------------------ |164| Simple field access | `name()`, `len()` | `get_name()`, `get_len()` |165| Fallible lookup | `get()`, `get_mut()` | `find()` (unless searching) |166| Boolean check | `is_empty()`, `has_key()`, `can_write()` | `empty()`, `key_exists()` |167| Setter | `set_name(value)` | `name(value)` (unless builder) |168169### Iterator Methods170171| Method | Yields | Ownership |172| ------------- | -------- | ------------------- |173| `iter()` | `&T` | Borrows collection |174| `iter_mut()` | `&mut T` | Mutably borrows |175| `into_iter()` | `T` | Consumes collection |176177Iterator type names match their method: `iter()` ->178`Iter`, `into_iter()` -> `IntoIter`, `keys()` -> `Keys`.179180### General Rules181182| Element | Convention | Example | Not |183| -------------------- | ----------------------- | -------------------- | -------------------- |184| Types, traits, enums | `UpperCamelCase` | `HttpServer` | `HTTPServer` |185| Enum variants | `UpperCamelCase` | `NotFound` | `NOT_FOUND` |186| Functions, methods | `snake_case` | `parse_json()` | `parseJSON()` |187| Constants, statics | `SCREAMING_SNAKE_CASE` | `MAX_RETRIES` | `maxRetries` |188| Lifetimes | Short lowercase | `'a`, `'de`, `'src` | `'input_lifetime` |189| Type params | Single uppercase | `T`, `E`, `K`, `V` | `ElementType` |190| Acronyms | Treat as words | `Uuid`, `HttpClient` | `UUID`, `HTTPClient` |191| Crate names | No `-rs`/`-rust` suffix | `json-parser` | `json-parser-rs` |192193---194195## Usage Scenarios196197### Scenario 1: Designing a Library Config Type198199You need a `Config` struct with 3 required fields and 5200optional fields.2012021. Use a builder with typestate for the 3 required fields2032. Add `#[must_use]` to the builder type2043. Derive `Debug, Clone, PartialEq, Default` on `Config`2054. Add `#[non_exhaustive]` if the struct is public and may206 gain fields2075. Gate serde behind a feature flag2086. Document with `# Examples` showing builder usage209210### Scenario 2: Adding Conversion Methods to a Newtype211212You have `struct Email(String)`:213214```rust215impl Email {216 /// Returns the email as a string slice. O(1), no allocation.217 pub fn as_str(&self) -> &str {218 &self.0219 }220221 /// Returns a new lowercase version. Allocates.222 pub fn to_lowercase(&self) -> Email {223 Email(self.0.to_lowercase())224 }225226 /// Consumes the Email, returning the inner String.227 pub fn into_string(self) -> String {228 self.0229 }230231 pub fn is_valid(&self) -> bool {232 self.0.contains('@')233 }234}235```236237### Scenario 3: Extending an External Type238239You need hex encoding for byte slices:240241```rust242pub trait ByteSliceExt {243 fn as_hex(&self) -> String;244}245246impl ByteSliceExt for [u8] {247 fn as_hex(&self) -> String {248 self.iter().map(|b| format!("{b:02x}")).collect()249 }250}251```252253Import `use my_crate::ByteSliceExt;` to use. Name the254trait with `Ext` suffix.255256---257258## Reference Index259260| Reference | Read When |261| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |262| [api-patterns](references/patterns.md) | Implementing builders, choosing #[must_use]/#[non_exhaustive], extension traits, Default, common traits, serde gating |263| [api-naming](references/naming.md) | Naming methods, types, conversions, iterators, or crate names |264| [api-documentation](references/documentation.md) | Writing doc comments, examples, error/panic/safety sections, intra-doc links, Cargo.toml metadata |265266---267268## Cross-References269270| Need | Skill |271| ---------------------------------------- | -------------- |272| Error types for Result-returning APIs | rust-errors |273| Newtype patterns, typestate, PhantomData | rust-types |274| Trait design, generics, dispatch | rust-types |275| Testing doc examples | rust-quality |276| Clippy lints for API quality | rust-quality |277| Ownership decisions in API signatures | rust-ownership |