Public API Design Patterns
Use this recipe when creating Rust libraries, SDK-like crates, reusable modules, or public APIs.
Constructor and Validation
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserId(String);
impl UserId {
/// Creates a user identifier.
///
/// # Errors
///
/// Returns an error when the identifier is blank.
pub fn new(value: impl Into<String>) -> Result<Self> {
let value = value.into();
if value.trim().is_empty() {
return Err(ServiceError::invalid_input("user id cannot be blank"));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}Builder Pattern
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub endpoint: String,
pub timeout: std::time::Duration,
}
#[derive(Debug, Default)]
pub struct ClientOptionsBuilder {
endpoint: Option<String>,
timeout: Option<std::time::Duration>,
}
impl ClientOptionsBuilder {
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn build(self) -> Result<ClientOptions> {
Ok(ClientOptions {
endpoint: self
.endpoint
.ok_or_else(|| ServiceError::invalid_input("endpoint is required"))?,
timeout: self.timeout.unwrap_or_else(|| std::time::Duration::from_secs(30)),
})
}
}API Rules
- Implement common traits where sensible:
Debug,Clone,Default,PartialEq,Eq,Hash,Serialize,Deserialize. - Use
From,TryFrom,AsRef, andAsMutinstead of ad-hoc conversion names. - Prefer newtypes for identifiers, flags, and constrained strings.
- Avoid boolean parameters when a small enum communicates intent better.
- Keep struct fields private for public library types unless direct field access is part of the contract.
- Use
#[non_exhaustive]for public enums and structs that may grow. - Make traits object-safe when downstream users are likely to need trait objects.
- Document errors, panics, and safety invariants.
- Avoid exposing dependency-specific types unless the dependency is intentionally part of the public API.
Panic Documentation
/// Returns the configured shard.
///
/// # Panics
///
/// Panics when `index >= self.shard_count()`. Prefer `get_shard` when handling user input.
pub fn shard(&self, index: usize) -> &Shard {
&self.shards[index]
}Prefer fallible alternatives for user-controlled input:
pub fn get_shard(&self, index: usize) -> Option<&Shard> {
self.shards.get(index)
}