Skip to main content

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:

  1. Initially you may use the @postman-app-monolith/* import alias to bridge the gap
  2. The package is tagged monolith-coupled in its Nx tags
  3. A follow-up PR removes the monolith imports and re-tags the package monolith-decoupled

See Migration strategy for the full process.


Import aliases reference

AliasResolves toWhen to use
@postman-app/<name>Nx package via node_modules symlinkAll 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

# Find all existing boundary violations
yarn knip