Re-exports and API Design
🎯 In Python, a well-designed library lets users import from the top level:
from mylib import User, Config, AppError
Even though internally User might live in mylib.internal.models.user. You achieve this by importing into __init__.py:
# mylib/__init__.py
from .internal.models.user import User
from .internal.config import Config
from .internal.errors import AppError
Rust uses the same pattern with pub use. You organize code internally however makes sense, then create a clean public API with re-exports. The internal structure is hidden — users never see it.
The prelude pattern
Many Rust libraries provide a prelude module — a single import that brings all the commonly-used types into scope:
// In your library
pub mod prelude {
pub use crate::Config;
pub use crate::Error;
pub use crate::Client;
}
// Users write:
use my_lib::prelude::*;
This is like Python's "import everything you need" idiom, but organized into a dedicated module. The prelude convention signals: "these are the essentials — import all of them."
Standard library examples: std::io::prelude::*, std::prelude::* (auto-imported). Popular crates like tokio, serde, and diesel all provide preludes.
Flattening deep module paths
Without re-exports, users navigate your internal structure:
// Without re-exports — exposes internals
use my_lib::internal::types::User;
use my_lib::internal::errors::ValidationError;
With pub use at the crate root:
// With re-exports — clean API
use my_lib::User;
use my_lib::ValidationError;
The internal structure can change freely without breaking callers. This is the same benefit Python's __init__.py provides, but with compiler-enforced privacy — if internal isn't pub, users literally can't reach into it.
Combining re-exports with validation
A common pattern: keep the struct's new() constructor in an internal module (where it can access private fields) and re-export the struct at the top level. Users interact with the clean API while the implementation details stay hidden:
mod internal {
pub mod inventory {
pub struct Product { pub name: String, pub price: f64 }
impl Product {
pub fn new(name: &str, price: f64) -> Result<Product, InvalidData> {
// validation logic here
}
}
}
}
pub use internal::inventory::Product; // users see: my_lib::Product
Cross-module imports with super::
When types needs to use errors from a sibling module, it reaches through the parent:
mod internal {
pub mod inventory {
use super::super::internal::warnings::InvalidData;
// or simply: use crate::internal::warnings::InvalidData;
}
pub mod warnings { /* ... */ }
}
Using crate:: (absolute path) is often clearer than chaining super:: when the path gets deep.
Login to see the full task and start coding.
Topics
This is a premium exercise
Log in to unlock the full exercise and start coding.
Login to access this exercise