Purpose & Principles
Goal: Build a scalable, maintainable monorepo for TypeScript, React, Elysia.js ( or NestJS) — optimized for speed and clarity as the team and codebase grow.
Guiding principles:
-
Single source of truth for shared code → packages live in
/packages
. - Apps are independent, only depend on packages.
- Strict boundaries enforced by Nx rules to prevent spaghetti dependencies.
- Smart automation → generate, lint, build, and test only what changed.
- Developer happiness → fast feedback, consistent tooling, minimal cognitive overhead.
Why This Matters
When multiple teams and apps share a codebase, things can spiral out of control quickly:
- Apps start depending on each other → deployment coupling and hidden breakage.
- Shared logic gets duplicated or inconsistently implemented.
- Build times explode as the repo grows.
- Onboarding new developers becomes painful — they don’t know where code should live or what’s safe to change.
By following this structure:
-
Independent apps
- You can deploy, test, and scale each app without worrying about others.
- Example: Your LMS web app doesn’t break if someone changes the e-commerce backend.
-
Single source of truth in
/packages
- Shared utilities, models, contracts, and UI components live in one place.
- No code duplication → no “bug fixed here but missed there”.
-
Fast pipelines with Nx caching
- Nx knows exactly which code depends on what → it only rebuilds and tests the minimal set of affected projects.
- This keeps CI and local dev blazing fast.
Problems This Solves
Problem | How This Doc Solves It |
---|---|
Apps depend directly on each other | Strict rule: Apps can only depend on packages, never other apps. |
Code duplication across apps | Centralize shared code in /packages . |
Confusing folder structures | Clear, predictable conventions for folder layout and naming. |
Slow builds & tests | Nx affected graph + smart caching = only run what changed. |
Fragile FE/BE type syncing | Shared contracts package with Zod → FE/BE stay type-safe. |
Env chaos | Clear rules for .env handling and config packages. |
New dev onboarding takes weeks | Generators, clear docs, and tags → consistent, fast ramp-up. |
Workspace Layout
apps/ # Standalone deployable apps
web/ # React frontend (Next.js or Vite)
admin/ # Admin dashboard
marketing/ # Public marketing site
mobile/ # React Native app
api-nest/ # NestJS backend service
api-scheduler/ # Background jobs service
packages/ # Reusable code (FE & BE)
shared/
util/ # Pure utility functions (no env or network access)
models/ # Shared TypeScript models
contracts/ # Zod schemas + API contracts
lms/ # LMS domain
feature/ # State orchestration / hooks
ui/ # UI components
data-access/ # API clients (frontend) or DB repositories (backend)
util/ # LMS-specific helpers
ecommerce/ # E-commerce domain
feature/
ui/
data-access/
util/
tools/ # Custom Nx generators + scripts
Core Rules
1 — Apps are Standalone
- Apps live in
/apps/
. - They cannot import anything from another app.
- They only import from
/packages
. - This ensures clean deployment boundaries and eliminates hidden coupling.
Example (✅ Allowed):
apps/web → imports → packages/lms-ui
apps/web → imports → packages/shared-contracts
Example (❌ Forbidden):
apps/web → imports → apps/admin
apps/admin → imports → apps/api-nest
2 — Packages Are the Source of Truth
- All shared code lives in
/packages/
. -
apps/
are thin shells that glue together packages and runtime configuration.
Naming, Tags & Boundaries
Package Naming
Format: <scope>-<type>
-
scope = domain (
lms
,ecommerce
,shared
) -
type ∈ (
feature
,ui
,data-access
,util
,models
,contracts
)
Examples:
-
lms-ui
,lms-data-access
,shared-contracts
.
Enforcing Boundaries with Nx
// root .eslintrc.js
rules: {
"@nx/enforce-module-boundaries": [
"error",
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{ "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:package"] },
{ "sourceTag": "type:ui", "onlyDependOnLibsWithTags": ["type:util","type:models","type:contracts","scope:shared","scope:<same>"] },
{ "sourceTag": "scope:shared", "onlyDependOnLibsWithTags": ["scope:shared"] }
]
}
]
}
Contracts & FE/BE Sync
// packages/shared/contracts/course.ts
import { z } from "zod";
export const Course = z.object({
id: z.string().uuid(),
title: z.string(),
published: z.boolean(),
});
export type Course = z.infer<typeof Course>;
- Frontend: use these schemas to type API clients and validate responses.
- Backend (Nest or Elysia): use schemas for input/output validation.
Environment & Config Rules
- Apps own their
.env
files. - Packages must never read directly from
process.env
. -
Create a
config
package that:- Reads env variables at runtime.
- Validates them with Zod.
- Exports a strongly-typed config object.
Caching, Targets & Bun
1 — Default Targets (nx.json
)
{
"targetDefaults": {
"build": { "cache": true },
"test": { "cache": true },
"lint": { "cache": true },
"typecheck": { "cache": true }
}
}
2 — Bun Commands
bun i # Install dependencies
bun run nx serve web # Run a frontend app
bun run nx serve api-nest # Run backend
bun run nx affected -t lint,typecheck,test,build
Affected Workflow
- Local Dev:
bun run nx affected:graph
- Before Commit:
bun run nx affected -t lint,test,typecheck
- CI:
- run: bun run nx affected -t lint,typecheck,test,build --parallel
Testing Strategy
-
Unit tests → close to source in
__tests__
. - Component tests → React Testing Library.
- E2E tests → Playwright for apps.
- Contract tests → Validate BE against Zod contracts.
Performance & DX Tips
- Use barrel files (
index.ts
) to curate package exports. - Keep packages small and domain-focused.
- Turn packages that need Node consumption into
buildable: true
. - Use
tsup
oresbuild
for tiny backend bundles.
Why Nx Over Competitors
Feature | Nx | Turborepo | Lerna |
---|---|---|---|
Dependency graph | ✅ Auto | ❌ Manual | ❌ None |
Task scheduling & caching | ✅ Smart (local + remote) | ⚠️ Limited | ❌ |
Built-in generators | ✅ Yes | ❌ No | ❌ |
Enforceable boundaries | ✅ Yes | ❌ No | ❌ |
Polyglot support (React, Elysia, Nest) | ✅ Excellent | ⚠️ Limited | ❌ |
Remote caching (team-wide) | ✅ Built-in | ⚠️ Third-party | ❌ |
Quick Commands Cheat Sheet
bun run nx g @nx/js:lib lms-ui --directory=lms --tags="scope:lms,type:ui,platform:browser"
bun run nx graph
bun run nx affected -t test
bun run nx serve web
Final Thoughts
By enforcing these practices:
- Each app remains deployable and testable on its own.
- Shared logic lives in packages, not copied between apps.
- Nx automatically keeps builds and tests fast even as the codebase grows.
- The team has clear rules, reducing onboarding friction and bugs.
Top comments (0)