Re-export Dependencies Whose Types Appear in Your API

Understand why exposing types from your dependencies demands re-exporting them, how version conflicts arise, and the strategies to keep your API usable and robust.

A crate’s public API is a contract with its users. When that contract includes types from a dependency—say a function accepts some_dep::SomeType—you have created a public dependency. The moment a downstream crate interacts with your API, it also interacts with that dependency’s types. If the downstream crate uses a different version of the same dependency, the Rust compiler sees those types as completely unrelated, leading to cryptic errors.

Re-exporting those types through your own crate solves the mismatch.

The problem — version mismatch with public types

Imagine you maintain a library named number_picker that picks random numbers. Internally it depends on rand version 0.7, and one of your public functions accepts an Rng trait from that version.

use rand::Rng;
/// Pick a number between 0 and n (exclusive) using
/// the provided `Rng` instance.
pub fn pick_number_with<R: Rng>(rng: &mut R, n: usize) -> usize {
    rng.gen_range(0, n) // Method from rand 0.7.x
}

A user of your crate writes a binary that also depends on rand, but version 0.8. The binary creates a random number generator and tries to pass it to your function.

fn main() {
    let mut rng = rand::thread_rng(); // from rand 0.8
    let choice = number_picker::pick_number_with(&mut rng, 10);
}

The compiler rejects this with an error that feels disconnected from reality:

error[E0277]: the trait bound `ThreadRng: rand_core::RngCore` is not satisfied

The text mentions rand_core::RngCore, which does exist, but the real issue is that the Rng trait in rand 0.8 is a completely different trait from the one in 0.7. The compiler sees ThreadRng from 0.8 and R: Rng expecting 0.7's trait. Cargo can happily link multiple versions of the same crate into one binary, but types from different versions are incompatible.

Public dependency without re-export leads to confusion:

When your crate's public API uses a dependency's type without re-exporting it, users must depend on the exact same version of that dependency. If they don't—or can't—the error messages rarely point clearly to the version mismatch. This is a frequent source of support issues for library authors.

The binary user now has to add the exact same rand 0.7 as a direct dependency, which might conflict with their own use of 0.8. This is a fragile chain: the user must reverse-engineer which version your library needs and keep it in sync manually.

How re-exporting fixes the mismatch

The library author can remove this burden by re-exporting the dependency. When you re-export a crate, you give downstream code a path to the exact version you use.

// Re-export the entire `rand` crate as part of our public API.
pub use rand;
use rand::Rng;
pub fn pick_number_with<R: Rng>(rng: &mut R, n: usize) -> usize {
    rng.gen_range(0, n)
}

Now the binary can reach the library’s rand version through number_picker::rand:

fn main() {
    let mut rng = number_picker::rand::thread_rng(); // rand 0.7
    let choice = number_picker::pick_number_with(&mut rng, 10);
}

The call to thread_rng() returns an Rng instance from the exact version that pick_number_with expects. The compiler sees matching types, and the error disappears.

Re-exporting resolves version silos:

By making the dependency available under your crate’s namespace, you give users a single source of truth for the version your API needs. They no longer have to add a separate dependency or guess version numbers.

This works because pub use rand; brings the entire rand crate into the public module tree of number_picker. Users can access anything the library’s version of rand exposes, from traits to free functions.

Re-export strategies — whole crate vs. specific items

You have two main ways to re-export a dependency’s types.

Re-export the entire cratepub use some_dep;

This is the simplest approach when your API relies on multiple items from that dependency. Users get full access to the dependency’s module tree under your crate’s root. It avoids peppering your API with dozens of individual re-exports. The trade‑off is that you expose a much larger surface: everything some_dep makes public becomes part of your crate’s visible API.

pub use serde; // Re-exports the whole serde crate

Downstream code can then use your_crate::serde::Serialize.

Re-export specific itemspub use some_dep::SpecificType;

If only a few types appear in your public signatures, re-exporting just those items keeps your namespace clean. You give users exactly what they need without pulling in the entire dependency’s API.

pub use url::Url;
pub use url::ParseError;
pub fn parse_config(s: &str) -> Result<Url, ParseError> { /* ... */ }

Users can then do use your_crate::Url; without knowing about the url crate at all.

No single correct answer:

Both strategies are widely used. Crates like hyper re-export http types extensively because they are core to the API. Other crates re-export only one or two enums. Choose based on how many types from the dependency users will need to interact with.

Semver implications of re-exporting

When you re-export a dependency’s types, that dependency becomes part of your crate’s public API. Under semantic versioning, a major version bump of the dependency should trigger a major version bump of your own crate. The user’s code may compile against your re-exported some_dep::SomeType, and that type’s definition can change across major versions of some_dep.

If the dependency is still on a 0.x version (where semver allows breaking changes in minor releases), you may need to follow suit more aggressively.

Re-export creates version coupling:

Before you re-export, consider whether you are comfortable with your crate’s version being tied to the dependency’s. If the dependency moves slowly and is widely used (e.g., url, regex), the coupling is manageable. For fast‑moving 0.x crates, re-exporting can force frequent major bumps on your own crate.

When to avoid re-exporting — wrapping instead

Re-exporting is not always the right answer. If the dependency’s type is an implementation detail that you don’t want users to depend on directly, wrap it in your own type.

// Instead of exposing rand::Rng, create a facade.
pub struct RandomPicker {
    rng: Box<dyn rand::RngCore>,
}
impl RandomPicker {
    pub fn pick(&mut self, max: usize) -> usize {
        self.rng.gen_range(0..max)
    }
}

Wrapping hides the dependency entirely. Your users never import rand. The trade‑off is that they lose the ability to pass in their own Rng implementations unless you design an adapter. Choose wrapping when:

  • The dependency’s types are an internal concern.
  • You want to insulate users from version churn completely.
  • You only need to expose a small, well‑defined behavior.

Re-exporting is the better choice when the dependency’s types are a natural part of the domain you model—for example, RecordBatch from the arrow crate, or Url from the url crate—and you want seamless interop with other crates in the ecosystem.

Documentation and #[doc(inline)]

When you re-export an item, rustdoc normally shows it as a re-export: a link to the original location. That can make your documentation feel fragmented. You can use #[doc(inline)] to inline the item’s documentation so it appears as if it were defined directly in your crate.

#[doc(inline)]
pub use rand::Rng;

In the generated docs, Rng will be shown at the re-export location with all its methods and trait impls visible, rather than a link to the rand crate’s page. This gives users a cohesive view of your API.

Inline documentation for cohesive API:

Using #[doc(inline)] makes your crate’s documentation page self‑contained. Users don’t need to jump to the dependency’s docs to understand how to use the types you expose.

Common mistakes

Forgetting to re-export when you have a public dependency — This is the most frequent error. Your crate compiles and tests pass because you test against your own dependency version. But the first user who has a different version in their tree gets a baffling trait‑bound error.

Re-exporting with a type alias instead of pub use — You can write pub type Foo = some_dep::Foo; to re‑export a type. This works for plain structs and enums, but fails in surprising ways for unit‑ or tuple‑structs. The alias can’t be used as a constructor:

pub type MyError = some_dep::Error; // fine
let e = some_dep::Error::new(); // works through alias? No.

For unit structs and tuple structs, let x = some_dep::Foo; or some_dep::Foo(42) won’t compile when Foo is only available as a type alias. Prefer pub use for re‑exports; reserve type aliases for genuinely new type names.

Type alias ≠ re-export for constructors:

A type alias cannot be used to construct a unit struct or tuple struct. This is a breaking change if you migrate from a real struct to a type alias, and it surprises library authors who think the two are equivalent. Always use pub use for re‑exporting items.

Re-exporting a dependency that does not appear in your API — Re‑exporting just for convenience (e.g., so users can use your_crate::tokio without extra Cargo.toml lines) is rarely justified. It commits you to that dependency’s version even though your own signatures don’t require it. Reserve re‑exports for types that are genuinely part of your public interface.

Summary

When a type from one of your dependencies appears in your public API, re‑export it. The act of re‑exporting gives users a clear path to the exact version your crate expects, avoiding silent version mismatches and cryptic compiler errors. Choose between re‑exporting the entire crate or specific items based on how many types users need. Be aware that re‑exporting creates a semver coupling between your crate and the dependency; if the dependency moves fast, consider wrapping the types instead.