Dependency rules
The most critical architectural rule in postman-app is the unidirectional dependency graph: dependencies flow downward only. A layer can import from layers below it, but never from layers above.
These rules are not just conventions — they are enforced by the build. Violating them causes a lint error that fails CI.
The rule at a glance
apps/ → can import anything below
views/ → can import ui-features, data, platform-*, libs
ui-features/ → can import data, platform-*, libs, other ui-features*
data/ → can import platform-libs, libs
platform-ui/ → can import platform-libs, libs
platform-libs/ → can import libs only
libs/ → can import external npm only
*ui-features importing other ui-features is allowed but must be done carefully to avoid circular dependencies. Prefer keeping ui-features independent.
Why these rules exist
Without enforced boundaries, code in a large codebase eventually develops circular dependencies — A imports B, B imports C, C imports A. This causes:
- Unpredictable initialization order (runtime errors)
- Inability to tree-shake unused code (bloated bundles)
- Tests that are difficult to isolate
- Impossible to extract packages into separate repos later
The monolith (src/renderer/) has no enforced boundaries and has accumulated thousands of circular imports over the years. The Nx layer system is the fix.
Correct examples
// ✅ ui-features/collections-sidebar importing from data/
import { CollectionStore } from '@postman-app/collection-data';
// ✅ data/ importing from platform-libs/
import { gatewayClient } from '@postman-app/gateway-service';
// ✅ platform-libs/ importing from libs/
import { retryFetch } from '@postman-app/fetch-with-retry';
// ✅ views/ importing from ui-features/
import { CollectionsSidebar } from '@postman-app/collections-sidebar';
Incorrect examples
// ❌ data/ importing from ui-features/ — data never knows about UI
import { something } from '@postman-app/collections-sidebar';
// ❌ libs/ importing from platform-libs/ — libs has no app-specific deps
import { i18n } from '@postman-app/i18n-sdk';
// ❌ ui-features/ importing from views/ — features don't know about routing
import { WorkspaceOverview } from '@postman-app/workspace-overview';
// ❌ platform-libs/ importing from data/ — infra doesn't know about domain stores
import { workspaceStore } from '@postman-app/workspace-data';
The monolith exception
src/renderer/ code does not follow these rules — it predates them. If you are extracting code from the monolith into an Nx package:
- Initially you may use the
@postman-app-monolith/*import alias to bridge the gap - The package is tagged
monolith-coupledin its Nx tags - A follow-up PR removes the monolith imports and re-tags the package
monolith-decoupled
See Migration strategy for the full process.
Deliberate re-coupling (the other direction)
The process above assumes coupling is transitional — a package arrives coupled and
becomes decoupled. Very occasionally a package goes the other way: it is already
monolith-decoupled and is deliberately re-tagged monolith-coupled because
composing host capabilities is its job.
This costs a real guard rail. Re-tagging removes the lint rule that keeps monolith imports out, so nothing mechanical stops the next import. It must therefore be an explicit, reviewed decision rather than a side effect of a feature, and it must be recorded here.
Before re-coupling a package, establish that:
- No decoupled package imports it. Otherwise the coupling spreads transitively and silently — the lint guard fires on monolith paths, not on imports of a coupled workspace package, so it will not catch the cascade.
- The imports are confined to one module, so the blast radius is visible and the eventual decoupling is a single re-point.
- A
__DECOUPLE_FIRST__README.mdrecords why, what each import is for, and what decoupling would take.
| Package | Why | Recorded |
|---|---|---|
ui-features/rich-text-editor | Composes the four host service implementations for the editor's DI seam. The engine package stays decoupled and renders with nothing supplied; a third coupled package was considered and rejected. | CSDK-1818 |
Import aliases reference
| Alias | Resolves to | When to use |
|---|---|---|
@postman-app/<name> | Nx package via node_modules symlink | All new code — canonical import path |
@postman-app-monolith/* | src/* | Temporary bridge for packages mid-migration |
@@renderer/* | src/renderer/* | Legacy only — do not use in new code |
How the rules are enforced
Rules are configured in each package's project.json under the tags field:
{
"tags": ["type:ui-feature", "scope:collaboration", "monolith-decoupled"]
}
The ESLint rule @nx/enforce-module-boundaries reads these tags and applies the constraint matrix defined in .eslintrc.js at the repo root. Running nx lint <package> will show boundary violations.
Checking for violations
# Check a specific package
nx lint workspace-overview
# Check all packages
nx run-many --target=lint --all