"Project setup" sounds like a checklist of files to create, but the choices behind it - one service per repo or many, compiled output checked in or not, a hand-rolled layout or a generator-driven one - all trace back to a small number of underlying trade-offs.
This page is about those trade-offs: repo topology, the build/runtime split, and the coordination problem that monorepo tooling exists to solve.
A project's structure encodes three separable decisions - what counts as one deployable, where the compile-step boundary sits, and whether one git history holds one service or many - and most "which tool do we need" questions are really about one of those three.
Insight: Getting the topology decision wrong in either direction costs real time - a premature monorepo taxes every PR with coordination overhead, while a service split too late means untangling shared code that's already coupled in production.
When to Use This Model: Deciding whether a new service needs its own repo, choosing when to introduce workspaces or a task orchestrator, reasoning about why src/ and dist/ are separate, and understanding what a generator or template repo is actually standardizing.
Limitations/Trade-offs: No structure is free - a monorepo trades per-service simplicity for cross-cutting coordination cost, and a single-service repo trades that coordination cost for eventual code duplication once a second deployable shows up.
Related Topics: npm workspaces, Turborepo, Nx, service boundaries, scaffolding and templates.
Underneath the folder names, a project's structure is really encoding three things.
The first is a deployable boundary: what counts as one independently runnable, independently deployable unit of code - one package.json, one build, one process that gets deployed as a whole.
The second is the build/runtime split: the line between source you edit (src/, TypeScript) and the artifact that actually runs in production (dist/, compiled JavaScript) - a split that exists because Node doesn't execute TypeScript natively in most production setups, so compilation is a build-time step, never a runtime one.
The third is repo topology: whether one deployable lives alone in its own git history (a single-service repo) or multiple deployables and shared packages live together in one git history, coordinated through workspaces (a monorepo).
These three decisions are independent of each other in principle - you can have a monorepo with no shared build tooling, or a single-service repo with an elaborate build pipeline - but in practice they tend to move together, because the tooling that supports one often assumes the others.
A useful analogy: a repo's structure is a building's blueprint.
The blueprint doesn't do anything by itself, but it decides where new work is supposed to go - a new electrical outlet has an obvious place to attach because the wiring plan already exists - which is exactly what keeps a team from each inventing its own wiring as it goes.
Repo topology cascades directly into tooling choice, and understanding that cascade explains why certain tools appear together.
A single-service repo needs nothing beyond a package manager - there's only one package.json, one dependency graph, one build.
A monorepo needs workspaces (npm, pnpm, or Yarn's workspace protocol - see Workspaces & Monorepos) at minimum, because workspaces are what let one package depend on another package in the same repo without publishing it to a registry first.
But workspaces alone only solve linking - they tell the package manager where local packages live, not which ones actually changed, and not how to avoid rebuilding or retesting packages that didn't.
That's the specific gap task orchestrators like Turborepo and Nx fill: both build a dependency graph between workspace packages (not just a linking graph, a task graph - "building api requires shared to already be built") and use it to run only what a given change actually affects, caching the result of everything else.
That's why a monorepo without a task orchestrator still works correctly, just slower as it grows - the orchestrator is a performance and coordination layer on top of workspaces, not a replacement for them.
Scaffolding is the mechanism that turns a chosen structure into something that gets replicated consistently, rather than re-decided by whoever creates the next service.
npm init, a framework CLI (like the NestJS generator), or an internal template repo are all doing the same conceptual job at different levels of opinion: encoding a structure decision once so it doesn't have to be argued about, or subtly drift, every time someone starts a new project.
// turbo.json - encodes the task graph, not just the workspace links{ "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "test": { "dependsOn": ["build"] } }}
Config discovery is also part of the mechanics, not just human convenience: tools like tsc, ESLint, and Docker look for their config files by walking up the directory tree from wherever they're invoked, which is exactly why tsconfig.json, package.json, and Dockerfile conventionally live at a repo (or workspace package) root - placement is how they're found, not merely where they're organized for readability.
Monorepo tooling earns its cost on a curve, not a threshold - the right moment to adopt Turborepo or Nx correlates with deployable count, build time pain, and how often a shared package changes in lockstep with its consumers, not with a fixed number of packages.
Multi-Service Monorepo covers the specific boundary rules (what belongs in apps/ vs packages/, when a package should be extracted at all) that make that judgment concrete.
The realistic adoption path is incremental, not a single big-bang decision: teams typically start with a single-service repo, add a second deployable and reach for plain workspaces once code needs to be shared, and only add a task orchestrator once build or CI time - not package count alone - becomes the actual pain point.
Skipping straight to a heavily tooled monorepo for a two-package repo usually adds configuration and cognitive overhead without a corresponding benefit yet.
CI is where this decision compounds most visibly: a whole-repo pipeline rebuilds and retests everything on every change, while affected-based CI - which both Turborepo and Nx support - uses the same task graph to run only what a given commit's changed files could have touched, which is the difference between a five-minute and a forty-minute pipeline once a monorepo has a dozen packages.
There's a governance dimension worth naming too: consistent structure across services (the same place for src/server.ts, the same script names, the same health-check convention) is what makes on-call and onboarding fast across an organization with many services - any engineer who's worked in one service repo can navigate another, which is the real payoff of scaffolding beyond saving typing on day one.
Layer
Strength
Weakness
Best Fit
Single-service repo, no orchestrator
Simplest possible setup, zero coordination tax
Code duplication once a second deployable appears
One deployable, small team
Workspaces only (no task orchestrator)
Shares code across packages with one lockfile
No caching or affected-based CI - full rebuilds every time
Small monorepo, 2-4 packages, tolerable build times
Turborepo
Simple pipeline config, fast to adopt incrementally
Less opinionated about code organization than Nx
Teams wanting caching/affected builds without a framework
Nx
Generators, enforced module boundaries, deep tooling integration
Steeper learning curve, more upfront structure
Larger orgs wanting enforced conventions across many teams
"A monorepo just means multiple repos managed together in git." It's the opposite - one git history containing multiple independently deployable or publishable packages, coordinated through workspaces and, usually, a task orchestrator.
"You need Turborepo or Nx the moment you have two packages." Plain workspaces are enough until build or test time, or the lack of change detection, becomes an actual bottleneck - adopting an orchestrator earlier just adds configuration to maintain.
"src/ vs dist/ is a style preference." It marks the compile-step boundary - what's edited by hand versus what actually ships and runs in production - and conflating the two makes it easy to accidentally deploy stale or uncompiled code.
"Scaffolding tools are just optional starter templates." They're how an organization enforces structural consistency at the moment a project is created, which is far cheaper than fixing structural drift across a dozen services later.
"A monorepo eliminates the need to think about versioning between packages." Internal packages can stay at workspace:* indefinitely inside the repo, but the moment any package is published or consumed outside the monorepo, real semver discipline is back on the table.
What three decisions does "project structure" actually encode?
Deployable boundaries (what's one independently deployed unit), the build/runtime split (source vs compiled output), and repo topology (single-service repo vs monorepo) - most concrete structure questions reduce to one of these three.
Why does Node code need a `src/` and `dist/` split at all?
Because TypeScript needs a compile step before it becomes plain JavaScript Node can run in most production setups - src/ is what's edited, dist/ is the artifact that's actually deployed and executed.
Do npm workspaces alone give me a monorepo build system?
They give you package linking - one package can depend on another in the same repo without publishing it - but they don't compute what changed or cache build results, which is the specific job a task orchestrator like Turborepo or Nx does on top.
When is it worth adopting Turborepo or Nx?
When plain workspace builds start taking real time because everything rebuilds on every change, or when a growing package count makes "what actually needs to run" hard to reason about manually - not simply at a fixed package count.
Why do config files like `tsconfig.json` conventionally live at the repo root?
Because tools look for them by walking up the directory tree from wherever they're invoked - root placement isn't just convention for humans, it's how the tooling actually finds the file.
What's the practical difference between Turborepo and Nx?
Both build a task graph and cache results, but Nx adds generators and enforced module-boundary rules on top, which suits larger organizations wanting consistency across many teams, while Turborepo stays lighter and faster to adopt incrementally.
Is starting with a single-service repo the "wrong" choice if the project might grow?
No - it's usually the right default, since a premature monorepo taxes every early PR with coordination overhead that doesn't pay off until there's a second deployable actually sharing code.
What does "affected-based CI" mean in practice?
The CI pipeline uses the same task graph the orchestrator uses for local builds to figure out which packages a given commit could have touched, and only builds/tests those - instead of rebuilding and retesting the entire repo on every change.
What's the actual value of a scaffolding tool or template repo beyond saving typing?
It encodes structural decisions - folder layout, script names, lint config, Docker setup - once, so every new service starts consistent instead of drifting slightly from whatever the last person who set one up remembered to include.
Does a monorepo mean internal packages never need version numbers?
Only while they stay purely internal - workspace:* resolves to the local copy without ever touching semver, but the moment a package is published externally or consumed outside the monorepo, real version discipline applies again.
How do I know when to extract a shared package instead of copying code?
A common rule of thumb is to tolerate duplication until a third consumer needs the same code - extracting after the second copy is often premature, and waiting for real, repeated duplication avoids guessing at an abstraction too early.