Crate Design

Public APIs and module visibility

intermediate
cratepubapi-design
šŸŽ® Interactive Playground

What is Crate Design?

Crate design involves structuring Rust libraries and binaries for maintainability, usability, and discoverability. Well-designed crates have clear APIs, good documentation, and sensible module organization.

The Problem

Poorly designed crates suffer from:

  • API confusion: Unclear what to use and how
  • Breaking changes: Minor updates break downstream code
  • Hidden features: Useful functionality buried in modules
  • Poor ergonomics: Verbose or awkward usage patterns

Example Code

// src/lib.rs - Main entry point
//! # MyAwesomeCrate
//!
//! A library for doing awesome things with Rust.
//!
//! ## Quick Start
//!
//!
rust

//! use myawesomecrate::prelude::*;

//!

//! let client = Client::builder()

//! .with_timeout(Duration::from_secs(30))

//! .build()?;

//!

//! let result = client.process("input").await?;

//!

//!
//! ## Features
//!
//! - `async` - Enable async support (default)
//! - `serde` - Enable serialization support
//! - `tracing` - Enable tracing instrumentation

#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]

// Re-export main types at crate root for convenience
pub use client::Client;
pub use config::Config;
pub use error::{Error, Result};

// Public modules
pub mod client;
pub mod config;
pub mod error;

// Feature-gated modules
#[cfg(feature = "serde")]
pub mod serialization;

// Internal modules (not pub)
mod internal;
mod utils;

/// Prelude module for convenient imports
pub mod prelude {
    pub use crate::client::{Client, ClientBuilder};
    pub use crate::config::Config;
    pub use crate::error::{Error, Result};

    // Re-export commonly used types from dependencies
    pub use std::time::Duration;
}

// src/error.rs - Error handling
//! Error types for the crate

use std::fmt;

/// The error type for all operations in this crate
#[derive(Debug)]
pub enum Error {
    /// Configuration error
    Config(ConfigError),
    /// Network error
    Network(NetworkError),
    /// Timeout error
    Timeout {
        /// Operation that timed out
        operation: &'static str,
        /// Timeout duration
        duration: std::time::Duration,
    },
    /// Invalid input
    InvalidInput(String),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Config(e) => write!(f, "configuration error: {}", e),
            Error::Network(e) => write!(f, "network error: {}", e),
            Error::Timeout { operation, duration } => {
                write!(f, "{} timed out after {:?}", operation, duration)
            }
            Error::InvalidInput(msg) => write!(f, "invalid input: {}", msg),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Config(e) => Some(e),
            Error::Network(e) => Some(e),
            _ => None,
        }
    }
}

/// Configuration-specific errors
#[derive(Debug)]
pub enum ConfigError {
    /// Missing required field
    MissingField(&'static str),
    /// Invalid value
    InvalidValue { field: &'static str, reason: String },
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::MissingField(field) => {
                write!(f, "missing required field: {}", field)
            }
            ConfigError::InvalidValue { field, reason } => {
                write!(f, "invalid value for {}: {}", field, reason)
            }
        }
    }
}

impl std::error::Error for ConfigError {}

/// Network-specific errors
#[derive(Debug)]
pub struct NetworkError {
    kind: NetworkErrorKind,
    message: String,
}

#[derive(Debug)]
enum NetworkErrorKind {
    ConnectionFailed,
    ConnectionReset,
    Dns,
}

impl fmt::Display for NetworkError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl std::error::Error for NetworkError {}

/// A specialized Result type for this crate
pub type Result<T> = std::result::Result<T, Error>;

// Convenient From implementations
impl From<ConfigError> for Error {
    fn from(err: ConfigError) -> Self {
        Error::Config(err)
    }
}

impl From<NetworkError> for Error {
    fn from(err: NetworkError) -> Self {
        Error::Network(err)
    }
}
// src/config.rs - Configuration with builder pattern
//! Configuration for the client

use crate::error::{ConfigError, Error, Result};
use std::time::Duration;

/// Client configuration
#[derive(Debug, Clone)]
pub struct Config {
    pub(crate) endpoint: String,
    pub(crate) timeout: Duration,
    pub(crate) retries: u32,
    pub(crate) headers: Vec<(String, String)>,
}

impl Config {
    /// Create a new configuration builder
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::new()
    }

    /// Get the configured endpoint
    pub fn endpoint(&self) -> &str {
        &self.endpoint
    }

    /// Get the configured timeout
    pub fn timeout(&self) -> Duration {
        self.timeout
    }
}

impl Default for Config {
    fn default() -> Self {
        Config {
            endpoint: "https://api.example.com".to_string(),
            timeout: Duration::from_secs(30),
            retries: 3,
            headers: Vec::new(),
        }
    }
}

/// Builder for creating Config instances
#[derive(Debug, Default)]
pub struct ConfigBuilder {
    endpoint: Option<String>,
    timeout: Option<Duration>,
    retries: Option<u32>,
    headers: Vec<(String, String)>,
}

impl ConfigBuilder {
    /// Create a new builder with default values
    pub fn new() -> Self {
        ConfigBuilder::default()
    }

    /// Set the API endpoint
    pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
        self.endpoint = Some(endpoint.into());
        self
    }

    /// Set the request timeout
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }

    /// Set the number of retries
    pub fn retries(mut self, retries: u32) -> Self {
        self.retries = Some(retries);
        self
    }

    /// Add a custom header
    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.push((name.into(), value.into()));
        self
    }

    /// Build the configuration
    pub fn build(self) -> Result<Config> {
        let endpoint = self.endpoint
            .ok_or(ConfigError::MissingField("endpoint"))?;

        // Validate endpoint
        if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
            return Err(ConfigError::InvalidValue {
                field: "endpoint",
                reason: "must start with http:// or https://".to_string(),
            }.into());
        }

        Ok(Config {
            endpoint,
            timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
            retries: self.retries.unwrap_or(3),
            headers: self.headers,
        })
    }
}
// src/client.rs - Main client with good API design
//! Client for interacting with the service

use crate::config::Config;
use crate::error::{Error, Result};
use std::time::Duration;

/// The main client for interacting with the service
///
/// # Example
///
///
rust,no_run

/// use myawesomecrate::{Client, Config};

/// use std::time::Duration;

///

/// # async fn example() -> myawesomecrate::Result<()> {

/// let client = Client::builder()

/// .endpoint("https://api.example.com")

/// .timeout(Duration::from_secs(30))

/// .build()?;

///

/// let response = client.get("/users").await?;

/// # Ok(())

/// # }

///

#[derive(Debug)]
pub struct Client {
    config: Config,
}

impl Client {
    /// Create a new client with the given configuration
    pub fn new(config: Config) -> Self {
        Client { config }
    }

    /// Create a client builder
    pub fn builder() -> ClientBuilder {
        ClientBuilder::new()
    }

    /// Perform a GET request
    pub async fn get(&self, path: &str) -> Result<Response> {
        self.request(Method::Get, path, None).await
    }

    /// Perform a POST request
    pub async fn post(&self, path: &str, body: impl Into<Body>) -> Result<Response> {
        self.request(Method::Post, path, Some(body.into())).await
    }

    /// Perform a PUT request
    pub async fn put(&self, path: &str, body: impl Into<Body>) -> Result<Response> {
        self.request(Method::Put, path, Some(body.into())).await
    }

    /// Perform a DELETE request
    pub async fn delete(&self, path: &str) -> Result<Response> {
        self.request(Method::Delete, path, None).await
    }

    async fn request(
        &self,
        method: Method,
        path: &str,
        body: Option<Body>,
    ) -> Result<Response> {
        // Implementation would go here
        let _ = (method, path, body);
        Ok(Response {
            status: 200,
            body: vec![],
        })
    }
}

/// Builder for creating Client instances
#[derive(Debug, Default)]
pub struct ClientBuilder {
    endpoint: Option<String>,
    timeout: Option<Duration>,
    retries: Option<u32>,
    headers: Vec<(String, String)>,
}

impl ClientBuilder {
    /// Create a new client builder
    pub fn new() -> Self {
        ClientBuilder::default()
    }

    /// Set the API endpoint
    pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
        self.endpoint = Some(endpoint.into());
        self
    }

    /// Set the request timeout
    ///
    /// Alias for `with_timeout` for API ergonomics
    pub fn timeout(self, timeout: Duration) -> Self {
        self.with_timeout(timeout)
    }

    /// Set the request timeout (alternative name)
    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }

    /// Set the number of retries
    pub fn retries(mut self, retries: u32) -> Self {
        self.retries = Some(retries);
        self
    }

    /// Add a custom header
    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.push((name.into(), value.into()));
        self
    }

    /// Build the client
    pub fn build(self) -> Result<Client> {
        let config = Config::builder()
            .endpoint(self.endpoint.unwrap_or_else(|| "https://api.example.com".to_string()))
            .timeout(self.timeout.unwrap_or(Duration::from_secs(30)))
            .retries(self.retries.unwrap_or(3));

        let config = self.headers.into_iter()
            .fold(config, |c, (k, v)| c.header(k, v))
            .build()?;

        Ok(Client::new(config))
    }
}

// Supporting types

#[derive(Debug, Clone, Copy)]
enum Method {
    Get,
    Post,
    Put,
    Delete,
}

/// Request body
#[derive(Debug)]
pub struct Body(Vec<u8>);

impl From<&str> for Body {
    fn from(s: &str) -> Self {
        Body(s.as_bytes().to_vec())
    }
}

impl From<String> for Body {
    fn from(s: String) -> Self {
        Body(s.into_bytes())
    }
}

impl From<Vec<u8>> for Body {
    fn from(v: Vec<u8>) -> Self {
        Body(v)
    }
}

/// Response from the API
#[derive(Debug)]
pub struct Response {
    /// HTTP status code
    pub status: u16,
    /// Response body
    pub body: Vec<u8>,
}

impl Response {
    /// Check if the response was successful (2xx)
    pub fn is_success(&self) -> bool {
        (200..300).contains(&self.status)
    }

    /// Get the body as a string
    pub fn text(&self) -> Result<String> {
        String::from_utf8(self.body.clone())
            .map_err(|_| Error::InvalidInput("Response is not valid UTF-8".to_string()))
    }

    /// Deserialize the body as JSON
    #[cfg(feature = "serde")]
    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
        serde_json::from_slice(&self.body)
            .map_err(|e| Error::InvalidInput(e.to_string()))
    }
}

# Cargo.toml - Feature organization
[package]
name = "myawesomecrate"
version = "1.0.0"
edition = "2021"
description = "A library for doing awesome things"
documentation = "https://docs.rs/myawesomecrate"
repository = "https://github.com/example/myawesomecrate"
license = "MIT OR Apache-2.0"
keywords = ["awesome", "rust", "example"]
categories = ["development-tools"]

[features]
default = ["async"]
async = ["tokio"]
serde = ["dep:serde", "dep:serde_json"]
tracing = ["dep:tracing"]
full = ["async", "serde", "tracing"]

[dependencies]
tokio = { version = "1.35", features = ["rt", "time"], optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
tracing = { version = "0.1", optional = true }

[dev-dependencies]
tokio = { version = "1.35", features = ["full", "test-util"] }

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

Why This Works

  1. Clear entry point: lib.rs shows what's public
  2. Prelude module: Common imports in one place
  3. Builder pattern: Ergonomic construction
  4. Feature flags: Optional functionality without bloat

Module Organization

src/
ā”œā”€ā”€ lib.rs          # Crate root, re-exports
ā”œā”€ā”€ prelude.rs      # Convenient imports
ā”œā”€ā”€ error.rs        # Error types
ā”œā”€ā”€ config.rs       # Configuration
ā”œā”€ā”€ client.rs       # Main functionality
ā”œā”€ā”€ internal/       # Private implementation
│   ā”œā”€ā”€ mod.rs
│   └── helpers.rs
└── tests/          # Integration tests

API Design Principles

| Principle | Example |

|-----------|---------|

| Sensible defaults | Config::default() works |

| Progressive disclosure | Basic usage is simple, advanced options available |

| Consistent naming | with_ or set_, not mixed |

| Type safety | Newtypes for domain concepts |

| Fail fast | Validation in builders |

āš ļø Anti-patterns

// DON'T: Expose internals
pub struct Client {
    pub connection_pool: Pool,  // Implementation detail!
}

// DO: Hide implementation
pub struct Client {
    pool: Pool,  // Private
}

// DON'T: Too many parameters
pub fn create(a: &str, b: u32, c: bool, d: Option<String>, e: &[u8]) { }

// DO: Use builder pattern
Client::builder()
    .option_a(a)
    .option_b(b)
    .build()

// DON'T: Generic error types
fn process() -> Result<(), Box<dyn Error>> { }

// DO: Specific error types
fn process() -> Result<(), ProcessError> { }

Exercises

  1. Add #[must_use] to appropriate methods
  2. Implement Debug that hides sensitive fields
  3. Add deprecation warnings for old API methods
  4. Create comprehensive documentation with examples

šŸŽ® Try it Yourself

šŸŽ®

Crate Design - Playground

Run this code in the official Rust Playground