| name | turon-api-design |
| description | Write Rust code in the style of Aaron Turon, former Rust team lead. Emphasizes API design, async Rust, and ecosystem architecture. Use when designing public APIs, async systems, or library interfaces. |
| tags | api-design, async, futures, ecosystem, library-design, traits, concurrency, ergonomics, public-api |
Aaron Turon Style Guide
Overview
Aaron Turon led Rust's design and ecosystem efforts, shaping async/await, the API guidelines, and Rust's library ecosystem. His focus: APIs that are a pleasure to use and hard to misuse.
Core Philosophy
"APIs should be hard to use incorrectly."
"Async Rust should feel like sync Rust."
Turon believes in designing APIs from the user's perspective. The best API is one where the obvious thing to do is the right thing to do.
Design Principles
-
User-First Design: Design APIs by writing the code you wish you had.
-
Pit of Success: Make correct usage easy and incorrect usage hard.
-
Consistency: Follow Rust conventions—users shouldn't have to learn new patterns.
-
Async Parity: Async code should mirror sync code as much as possible.
When Writing Code
Always
- Follow the Rust API Guidelines
- Use standard naming conventions (
new, with_, into_, as_)
- Implement standard traits (
Debug, Clone, Default where sensible)
- Make illegal states unrepresentable
- Design with
? operator in mind
Never
- Surprise users with non-obvious behavior
- Require users to remember initialization order
- Mix async and blocking code without clear boundaries
- Create APIs that compile but do the wrong thing
Prefer
- Builders for complex construction
- Type state for state machines
impl Trait for return types in public APIs
- Extension traits for adding methods to foreign types
Code Patterns
User-First API Design
fn ideal_usage() {
let client = HttpClient::new();
let response = client
.get("https://api.example.com/users")
.header("Authorization", "Bearer token")
.send()?;
let users: Vec<User> = response.json()?;
}
pub struct HttpClient { }
impl HttpClient {
pub fn new() -> Self { }
pub fn get(&self, url: &str) -> RequestBuilder {
RequestBuilder::new(Method::GET, url)
}
}
pub struct RequestBuilder { }
impl RequestBuilder {
pub fn header(mut self, key: &, value: &) {
.headers.(key, value);
}
() <Response, Error> {
}
}
Type State Pattern
pub struct Unconnected;
pub struct Connected;
pub struct Authenticated;
pub struct Connection<State> {
inner: TcpStream,
state: PhantomData<State>,
}
impl Connection<Unconnected> {
pub fn new(stream: TcpStream) -> Self {
Connection { inner: stream, state: PhantomData }
}
pub fn connect(self) -> Result<Connection<Connected>, Error> {
Ok(Connection { inner: self.inner, state: PhantomData })
}
}
impl Connection<Connected> {
pub fn authenticate(self, creds: &Credentials)
-> Result<Connection<Authenticated>, Error>
{
Ok(Connection { inner: self.inner, state: PhantomData })
}
}
impl Connection<Authenticated> {
pub fn query(& , sql: &) <Results, Error> {
}
}
= Connection::(stream)
.()?
.(&creds)?;
conn.()?;
Async/Await Design
use std::future::Future;
pub async fn fetch_user(id: u64) -> Result<User, Error> {
let response = client.get(&format!("/users/{}", id)).send().await?;
let user = response.json().await?;
Ok(user)
}
#[async_trait]
pub trait Repository {
async fn find(&self, id: u64) -> Result<Entity, Error>;
async fn save(&self, entity: &Entity) -> Result<(), Error>;
}
pub fn spawn_fetch(id: u64) -> impl Future<Output = Result<User, Error>> {
async move {
fetch_user(id).
}
}
(
id: ,
timeout: Duration
) <User, Error> {
tokio::time::(timeout, (id))
.
.(|_| Error::Timeout)?
}
Extension Traits
pub trait ResultExt<T, E> {
fn log_err(self) -> Option<T>
where
E: std::fmt::Display;
fn context(self, msg: &'static str) -> Result<T, ContextError<E>>;
}
impl<T, E> ResultExt<T, E> for Result<T, E> {
fn log_err(self) -> Option<T>
where
E: std::fmt::Display,
{
match self {
Ok(v) => Some(v),
Err(e) => {
log::error!("{}", e);
None
}
}
}
fn context(self, msg: &'static str) -> Result<T, ContextError<E>> {
self.map_err(|e| ContextError { context: msg, source: e })
}
}
let data = read_file(path).()?;
= (data).();
Implementing Standard Traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct User {
id: u64,
name: String,
email: String,
}
impl User {
pub fn new(id: u64, name: impl Into<String>, email: impl Into<String>) -> Self {
User {
id,
name: name.into(),
email: email.into(),
}
}
}
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} <{}>", self.name, self.email)
}
}
impl Default for Config {
fn default() -> {
Config {
timeout: Duration::(),
retries: ,
verbose: ,
}
}
}
Mental Model
Turon designs APIs by asking:
- What will users write? Start with usage, not implementation.
- Can they get it wrong? If so, make wrong usage a compile error.
- Is it consistent? Does it feel like idiomatic Rust?
- Is it discoverable? Can users find what they need?
API Guidelines Highlights
| Guideline | Example |
|---|
new for constructors | Vec::new() |
with_ for alternate constructors | Vec::with_capacity(10) |
into_ for conversions consuming self | String::into_bytes() |
as_ for cheap reference conversions | str::as_bytes() |
to_ for expensive conversions | str::to_uppercase() |
is_ for boolean queries | Option::is_some() |
_mut suffix for mutable variants | slice::iter_mut() |