skip to content

When splitting a Dart utilities package into a public API and `lib/src` internals, how do `export ... show` and `lib/src` decide what consumers depend on?

level: seniorimportance: should knowfreq 35%

answer

  1. one main library under lib
  2. export with show lists
  3. lib/src is a convention
  4. implementation_imports lint
  5. exported names are public API

basics

~20 s

Put implementation in lib/src and a main library at lib/<package>.dart that re-exports chosen names with export ... show. Consumers import the main file; lib/src is private by convention only, so anything exported is the real public API.

solid answer

~30 s

The pub layout convention puts implementation libraries under `lib/src/` and a main library at `lib/<package>.dart` that re-exports what consumers should use, ideally with `show` lists so nothing leaks by accident (`export 'src/slug.dart' show slugify;`). Consumers write one `import 'package:string_kit/string_kit.dart';`. `lib/src` is **not** enforced by the compiler: another package can import `package:string_kit/src/slug.dart`, and only the `implementation_imports` lint warns. What it buys is a **versioning promise**: maintainers may reshape `src` freely, while every exported name is public API regardless of which file declares it. `export` also does not import: the exporting file cannot use `slugify` unless it imports it too.

code

dart · 10 lines
dart
// lib/string_kit.dart — the only file consumers should import
/// Small string utilities.
library;

export 'src/case.dart' show toSnakeCase, toCamelCase;
export 'src/slug.dart' show slugify;
export 'src/truncate.dart' show truncate, Ellipsis;

// export does not import: needed only if this file uses slugify itself.
// import 'src/slug.dart';

go deeper

for a junior

Know the layout: public libraries directly under lib, implementation under lib/src, and one main file consumers import.

for a middle

Explain export with show and hide, that export is not import, and that lib/src is protected by a lint and a promise, not by the compiler.

for a senior

Design the public surface deliberately with show lists and treat every exported name as API, whatever file declares it.

for a principal

Decide how many public entry libraries a shared package offers and who approves additions to the exported surface across teams.

## The shape pub expects A Dart package makes everything under `lib/` importable by other packages as `package:<name>/<path>`. The pub layout convention divides that directory into two zones: - **Public libraries** directly under `lib/` (or in subfolders such as `lib/testing/`): files you intend people to import. - **Implementation files** under `lib/src/`: code the package itself uses. The docs describe it as private to the package's implementation: other packages "should never" import `src/...`. The usual centrepiece is a **main library** at `lib/<package-name>.dart` that **exports** the public API, so a consumer gets everything with one import. The dart.dev packages guide recommends many small **mini libraries** under `src`, one per class unless classes are tightly coupled, plus the occasional extra public library, such as one that depends on `dart:io`, or one meant to be imported with a prefix. ## `export`, `show` and `hide` `export 'src/slug.dart';` re-exposes every public name of `src/slug.dart` to whoever imports the exporting file. Combinators apply here too, and the shelf package is the dart.dev example of exporting each file with a **`show` list**: | Directive | Effect for importers of `string_kit.dart` | |---|---| | `export 'src/slug.dart';` | every public name in `slug.dart` | | `export 'src/slug.dart' show slugify;` | only `slugify` | | `export 'src/case.dart' hide CaseTable;` | everything except `CaseTable` | `show` lists are worth their upkeep: they give reviewers a single page listing the public surface, and a public helper added to a `src` file for internal reuse does not silently become API. Two mechanics people get wrong: 1. **Export is not import.** The exporting library does not gain the exported names in its own scope. If `string_kit.dart` also wants to call `slugify`, it needs an `import` as well. 2. **Privacy still applies.** Underscore names are library-private and can never be exported, so the tools compose: `_` hides within a file, `lib/src` hides from other packages by convention, and `export` chooses what crosses. ## What `lib/src` does and does not enforce Nothing in the compiler or in `pub get` blocks `import 'package:string_kit/src/slug.dart';` from another package. The protection is social and tooling-based: - the **`implementation_imports`** lint, in `package:lints`' recommended set (and so in `flutter_lints`), warns at the importing site; - Effective Dart explains the stakes: maintainers are free to make **sweeping changes under `src`** without it being a breaking change, so a consumer who reaches in can be broken by a minor release. The flip side binds the maintainer: once a name is exported from a public library, it **is public API**, even though its declaration lives in `src`. Renaming or removing it, or changing its signature, breaks consumers exactly as if it were declared in `lib/string_kit.dart`. Deciding how to version that change is the publishing leaf's subject. ## A worked split For a utilities package `string_kit` that grew into a single 2,000-line file: 1. Move each cohesive piece into `lib/src/slug.dart`, `lib/src/case.dart`, `lib/src/truncate.dart`, keeping helpers `_private` inside each. 2. Create `lib/string_kit.dart` with one `export ... show` per file listing only the intended names. 3. Offer an optional `lib/testing.dart` exporting fakes for consumers' tests. 4. Make the package's own tests import through `package:string_kit/...`, including `src` paths when unit-testing internals. 5. Run `dart pub publish --dry-run` and the analyzer before releasing, and treat any change to an exported name as an API change.

  • In a Dart package, a consumer imports `package:string_kit/src/slug.dart` directly and a minor release breaks them. Whose problem is it?
    The consumer's. Files under `lib/src` carry no API promise, and Effective Dart says maintainers may change them freely without a breaking release. The `implementation_imports` lint warned them at the import site; the fix is to use the exported API or request that the name be exported.
  • In Dart, when would a package expose a second public library beside `lib/<package>.dart`?
    When part of the API should not come with every import: a library that depends on `dart:io` while the main one is cross-platform, testing fakes in `lib/testing.dart`, or a set of names meant to be imported with a prefix. Each is still a public contract.

The main library is the shop counter and lib/src is the stockroom behind it. The door is not locked, so a customer can walk in, but nothing on those shelves is promised to be there next week; only what is on the counter is.

saying these in an interview costs you the question

  • The compiler refuses any package: import containing /src/
  • Exported names from lib/src are internal because the file is in src
  • export also makes the names usable inside the exporting file
  • A bare export of a src file is as safe as a show list
  • Private _names can be exported if listed in show