crefft Source 2026.10.4

Rust API Design

crefft > Languages > Rust > Rust API Design

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 RFC 2119 RFC 8174 when, and only when, they appear in all capitals, as shown here.

This guide covers the traits a public type MUST implement and the naming, documentation, and type-safety rules for a published Rust API, organized around the vendored Rust API Guidelines. The toolchain, lint, formatting, and pre-commit rules in the Rust Style Guide apply and are not repeated here, and so do the dependency and CI rules; every version quoted is listed in the Tested Version Matrix and was current on 8 September 2026.

Common Trait Implementations

Types MUST eagerly implement common traits. Rust’s orphan rule means a downstream crate cannot supply the implementation you left out.

Why This Matters

  • Orphan rule: a crate may implement a trait for a type only if it owns the trait or the type. If your crate publishes Config and a user wants Display for it, the user owns neither Display nor Config and cannot write that implementation at all; the vendored reference works the same example through with webapp, url::Url, and Display.1
  • You keep the ability, at a cost: the defining crate can add implementations later, but adding one is a compatibility event — inherent method resolution, inference, and downstream blanket implementations can all change — so adding Debug in 0.4 is not free either.
  • Ecosystem compatibility: types without Debug cannot be used in Result error positions, and #[derive(Debug)] on a struct fails if any field is not Debug, so one missing implementation propagates outwards.
  • User expectations: Rust developers expect types to be debuggable, cloneable, and comparable.
// GOOD: derive the full set up front. Users who need `Display` for their own
// wrapper can newtype it, but they will never need to.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
    namespace: String,
    id: u64,
}
// BAD: downstream cannot fix this. `impl Debug for CacheKey` in a user's crate
// is rejected: "only traits defined in the current crate can be implemented
// for types defined outside of the crate".
pub struct CacheKey {
    namespace: String,
    id: u64,
}

Required Traits Checklist

All public types MUST implement these traits where applicable:

// MINIMUM: Every public type needs Debug
#[derive(Debug)]
pub struct Config {
    timeout: Duration,
    retries: u32,
}

// RECOMMENDED: Add Clone, PartialEq, Eq when semantically valid
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserId(pub u64);

// FOR HASH MAPS: Add Hash when type will be used as a key
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
    namespace: String,
    id: u64,
}

// FOR VALUE OBJECTS: Add Copy when the type is small and Copy-safe
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Point {
    x: i32,
    y: i32,
}

Trait Implementation Guide

TraitWhen to ImplementNotes
DebugAlwaysRequired for error messages and debugging
CloneWhen duplication makes senseSkip for types with unique ownership (file handles)
PartialEq, EqWhen equality is meaningfulEq requires PartialEq
HashWhen used as HashMap/HashSet keyRequires Eq
DefaultWhen there’s a sensible defaultEnables ..Default::default() syntax
CopySmall, stack-only typesImplies Clone, changes move semantics
Send, SyncUsually automaticOverride only when unsafe is involved

Error Types

Error types have additional requirements:

use std::error::Error;
use std::fmt;

#[derive(Debug)]
pub struct ParseError {
    line: usize,
    message: String,
}

// Error types MUST implement Display
impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "parse error at line {}: {}", self.line, self.message)
    }
}

// Error types MUST implement std::error::Error
impl Error for ParseError {}

// Error types SHOULD be Send + Sync for use across threads
// (This is automatic if all fields are Send + Sync)

Conversion Traits

Implement standard conversion traits for interoperability:

// From for infallible conversions
impl From<u16> for PortNumber {
    fn from(value: u16) -> Self {
        PortNumber(value)
    }
}

// TryFrom for fallible conversions
impl TryFrom<u32> for PortNumber {
    type Error = PortRangeError;

    fn try_from(value: u32) -> Result<Self, Self::Error> {
        if value > 65535 {
            Err(PortRangeError(value))
        } else {
            Ok(PortNumber(value as u16))
        }
    }
}

// NEVER implement Into or TryInto directly - use From/TryFrom instead
// The blanket impl provides Into automatically

Serde Support

Libraries SHOULD provide Serde support behind a feature flag:

# Cargo.toml
[features]
default = []
serde = ["dep:serde"]

[dependencies]
serde = { version = "1.0", features = ["derive"], optional = true }
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Config {
    pub timeout_ms: u64,
    pub retries: u32,
}

API Design and Documentation

Public APIs MUST follow the Rust API Guidelines. crefft vendors the full text under reference/rust/; this section carries the rules that apply to every crate and links to the detailed page for each one.

Why

The guidelines encode what the standard library and the wider ecosystem already do. A crate that deviates costs its users on every call: they guess the wrong method name, cannot find the error conditions, and discover breaking changes at upgrade time. Following them is also cheaper than fixing them after publication, because correcting a public API can break existing callers. Release numbers follow the Versioning Guide, independently of compatibility.

Naming

Names MUST follow RFC 430 casing and the standard conversion prefixes.2

ConventionRuleExample
Casing (C-CASE)UpperCamelCase types, snake_case values, SCREAMING_SNAKE_CASE constantsstruct CacheKey, fn parse_retries, const MAX_RETRIES
Conversions (C-CONV)as_ borrowed, to_ expensive, into_ ownedas_str, to_owned, into_bytes
Getters (C-GETTER)No get_ prefix except for a single obvious accessorfn timeout(&self), not fn get_timeout(&self)
Iterators (C-ITER)iter, iter_mut, into_iterfn iter(&self) -> Iter<'_, T>
Iterator types (C-ITER-TY)Type name matches the methoditer() returns Iter
Features (C-FEATURE)No placeholder wordsserde, not use-serde or with-serde
Word order (C-WORD-ORDER)Consistent across the crateSocketAddrV4/SocketAddrV6, not V4SocketAddr
// GOOD: the prefix states the cost, and the getter has no `get_`.
impl Config {
    pub fn timeout(&self) -> Duration { self.timeout }
    pub fn as_str(&self) -> &str { &self.name }
    pub fn into_parts(self) -> (String, Duration) { (self.name, self.timeout) }
}
// BAD: `get_` prefix, and `to_parts` implies a cheap borrow of an owned value.
impl Config {
    pub fn get_timeout(&self) -> Duration { self.timeout }
    pub fn to_parts(self) -> (String, Duration) { (self.name, self.timeout) }
}

Documentation

Every public item MUST carry rustdoc. Documentation MUST state error, panic, and safety conditions, and examples MUST use ? rather than unwrap.3

/// Converts a port number, rejecting values outside the `u16` range.
///
/// # Errors
///
/// Returns [`PortRangeError`] when `value` exceeds `u16::MAX`.
///
/// # Examples
///
/// ```
/// use core_api::PortNumber;
///
/// let port = PortNumber::try_from(8080_u32)?;
/// assert_eq!(u16::from(port), 8080);
/// # Ok::<(), core_api::PortRangeError>(())
/// ```
fn try_from(value: u32) -> Result<Self, Self::Error> {
    u16::try_from(value).map(Self).map_err(|_| PortRangeError(value))
}
// BAD: no `# Errors`, and the example panics instead of propagating. A reader
// cannot tell what makes this fail, and `cargo test --doc` proves nothing
// about the error path.
/// Converts a port number.
///
/// ```
/// let port = PortNumber::try_from(8080u32).unwrap();
/// ```

Doc examples are tests. cargo test --doc MUST run in CI, so an example that stops compiling fails the build rather than rotting in place.

Manifests MUST carry the full metadata set (C-METADATA): description, license, repository, documentation, homepage, keywords, categories. Items that are public only for macro expansion MUST be hidden with #[doc(hidden)] (C-HIDDEN).

Future-Proofing

RuleMechanism
Sealed traits (C-SEALED)Private supertrait blocks downstream implementations
Private fields (C-STRUCT-PRIVATE)Accessors instead of public fields
Extensible enums and structs#[non_exhaustive]
Encapsulation (C-NEWTYPE-HIDE)Newtype over the concrete iterator or error
No duplicated bounds (C-STRUCT-BOUNDS)Bound the impl, not the struct
// GOOD: sealed. New methods on `Format` are not a breaking change, because
// nobody outside this crate can implement it.
mod sealed {
    pub trait Sealed {}
}

pub trait Format: sealed::Sealed {
    /// File extension, without the leading dot.
    fn extension(&self) -> &'static str;
}

pub struct Json;
impl sealed::Sealed for Json {}
impl Format for Json {
    fn extension(&self) -> &'static str { "json" }
}
// GOOD: adding a field later is compatible, because downstream cannot
// construct or exhaustively destructure this type.
#[non_exhaustive]
pub struct Request {
    pub path: String,
    pub headers: HashMap<String, String>,
}
// BAD: every field is public and the struct is exhaustive, so adding
// `query: String` breaks every `Request { path, headers }` pattern downstream.
pub struct Request {
    pub path: String,
    pub headers: HashMap<String, String>,
}

Dyn Compatibility

Traits that MAY usefully be used as dyn Trait MUST stay dyn-compatible (the property formerly called object safety): no generic methods, no Self return by value, no Self: Sized supertrait.4 Where both are wanted, provide the generic API as a blanket extension over the dyn-compatible core.

// GOOD: dyn-compatible core, generics moved to a free function.
pub trait Sink {
    fn write_line(&mut self, line: &str) -> std::io::Result<()>;
}

pub fn write_all<S: Sink + ?Sized>(sink: &mut S, lines: &[&str]) -> std::io::Result<()> {
    lines.iter().try_for_each(|line| sink.write_line(line))
}

pub fn make(boxed: Box<dyn Sink>) -> Box<dyn Sink> { boxed }
// BAD: the generic method makes `dyn Sink` impossible:
// "the trait `Sink` is not dyn compatible".
pub trait Sink {
    fn write_all<I: IntoIterator<Item = &'static str>>(&mut self, lines: I);
}

Type Safety

Arguments MUST convey meaning through types rather than bare bool or Option (C-CUSTOM-TYPE), and newtypes SHOULD be used to make distinct quantities distinct (C-NEWTYPE).5

// GOOD: unmistakable at the call site.
pub enum Overwrite { Yes, No }
pub fn save(path: &Path, overwrite: Overwrite) -> io::Result<()> { /* ... */ }

save(path, Overwrite::Yes)?;
// BAD: `save(path, true, false)` is unreadable and silently swappable.
pub fn save(path: &Path, overwrite: bool, fsync: bool) -> io::Result<()> { /* ... */ }

The full checklist, including predictability, dependability, and debuggability rules not repeated here, is in reference/rust/checklist.md.

References

See Also

Footnotes

  1. Interoperability - Rust API Guidelines, C-COMMON-TRAITS, and the orphan rule ↩

  2. Naming - Rust API Guidelines, RFC 430 casing, and conversion prefixes ↩

  3. Documentation - Rust API Guidelines, rustdoc requirements ↩

  4. Flexibility - Rust API Guidelines, C-OBJECT, and generic API design ↩

  5. Type safety - Rust API Guidelines, newtypes, and custom argument types ↩