Public APIs and module visibility
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.
Poorly designed crates suffer from:
// 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"]
lib.rs shows what's publicsrc/
āāā 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
| 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 |
// 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> { }
#[must_use] to appropriate methodsDebug that hides sensitive fieldsRun this code in the official Rust Playground