Pybites Logo Rust Platform

Re-exports and API Design

Medium +3 pts

🎯 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.

This is a premium exercise

Log in to unlock the full exercise and start coding.

Login to access this exercise