This post outlines the importance of applying Screaming Architecture and Collocation in both your back-end and front-end projects, and how this helps you and your teammates.
Open most Node.js projects, and you see this:
src/
βββ controllers/
βββ services/
βββ models/
βββ middleware/
βββ utils/
βββ routes/
What does this app do?
You see that itβs an Express app. But thatβs all the structure tells you.
Is it an e-commerce platform? A healthcare system? A social network?
Youβd have to open files and read code to find out.
Thatβs the problem.
What Is Screaming Architecture?
Uncle Bob coined the term:
Your project structure shouldscreamwhat the application does, not what framework it uses.
For example, a healthcare app should look like this:
src/
βββ patients/
βββ appointments/
βββ prescriptions/
βββ billing/
βββ shared/
You open the folder and immediately know: this is a healthcare system.
The domain is front and center.
Express, REST, GraphQL, Prisma, React, those are implementation details, tucked inside each feature.
And this is not just about aesthetics.
When folders map to business capabilities, you get:
- Discoverability: New developers find code faster.
- Ownership: Teams own features, not layers.
- Change isolation: Modifying βappointmentsβ doesnβt require touching 5 different layer folders.
- Deletion: You can remove an entire feature by deleting one folder.
- Low/High Coupling: Low coupling between features and High coupling for a single feature.

Or we could also look at it in another way to see more clearly how a single feature maps across different technical layers:

The Colocation Principle
In the React world, we must strive to:
Keep things as close as possible to where they're used.
Consider the following hierarchy:
- If only one component uses it β put it in that componentβs file
- If only components in one folder use it β put it in that folder
- If multiple folders use it β move it up to the nearest shared parent
- If the whole app uses it β put it in
shared/orlib/
This is the same idea as screaming architecture, applied at a granular level.
Don't prematurely extract.
Don't create utils/ and helpers/ folders that become junk drawers.
Combining Both: Feature-First Structure
Hereβs what a feature-first Node.js project looks like when you combine screaming architecture with colocation:
src/
βββ patients/
β βββ patient.controller.ts
β βββ patient.service.ts
β βββ patient.repository.ts
β βββ patient.types.ts
β βββ patient.validation.ts
β βββ patient.routes.ts
β βββ __tests__/
β βββ patient.service.test.ts
β βββ patient.controller.test.ts
βββ appointments/
β βββ appointment.controller.ts
β βββ appointment.service.ts
β βββ appointment.repository.ts
β βββ appointment.types.ts
β βββ appointment.validation.ts
β βββ appointment.routes.ts
β βββ __tests__/
β βββ appointment.service.test.ts
βββ shared/
β βββ middleware/
β β βββ auth.middleware.ts
β β βββ error-handler.ts
β βββ database/
β β βββ prisma.ts
β βββ types/
β βββ common.ts
βββ app.ts
βββ server.ts
Every feature is self-contained.
Tests live next to the code they test.
Shared infrastructure sits in shared/, but only things that are genuinely shared across features.
The Same Principle in React
src/
βββ features/
β βββ patients/
β β βββ PatientList.tsx
β β βββ PatientDetail.tsx
β β βββ usePatients.ts
β β βββ patient.types.ts
β β βββ patient.api.ts
β βββ appointments/
β β βββ AppointmentCalendar.tsx
β β βββ BookAppointment.tsx
β β βββ useAppointments.ts
β β βββ appointment.api.ts
βββ shared/
β βββ components/
β β βββ Button.tsx
β β βββ Modal.tsx
β βββ hooks/
β β βββ useAuth.ts
β βββ lib/
β βββ api-client.ts
βββ App.tsx
βββ main.tsx
Same idea.
Features own their components, hooks, types, and API calls.
shared/ contains only genuinely reusable pieces.
When Layers Still Make Sense
Layers arenβt always wrong. They work when:
- Your app is small (< 10 files). Feature folders are overhead for a simple CRUD API.
- Youβre building a library. Internal structure matters less than the public API.
- Your team is one person. You already know where everything is.
The pragmatic hybrid: features at the top level, layers within each feature.
Each feature has its own controller, service, repository, but theyβre colocated, not scattered.
Enforcing Boundaries
Structure without enforcement is just a suggestion.
Here's how to make it stick:
ESLint (with eslint-plugin-boundaries)
// .eslintrc.js
module.exports = {
plugins: ['boundaries'],
settings: {
'boundaries/elements': [
{ type: 'patients', pattern: 'src/patients/*' },
{ type: 'appointments', pattern: 'src/appointments/*' },
{ type: 'shared', pattern: 'src/shared/*' },
],
},
rules: {
'boundaries/element-types': [2, {
default: 'disallow',
rules: [
{ from: 'patients', allow: ['shared'] },
{ from: 'appointments', allow: ['shared', 'patients'] },
],
}],
},
};

TypeScript Project References
// src/shared/tsconfig.json β every referenced project needs composite: true
{
"compilerOptions": {
"composite": true,
"rootDir": ".",
"outDir": "../../dist/shared"
}
}
// src/patients/tsconfig.json
{
"compilerOptions": {
"composite": true,
"rootDir": ".",
"outDir": "../../dist/patients"
},
"references": [
{ "path": "../shared" }
]
}
Each feature is a TypeScript project.
It can only reference what's listed in references.
The compiler enforces boundaries at build time.
π Key Takeaways
- Your folder structure is documentation. It should tell a new developer what the app does in 5 seconds.
- Organize by feature, not by layer. Controllers, services, and models for the same feature belong together.
- Colocate aggressively. Tests, types, and utilities live next to the code that uses them. Move things to
shared/only when two or more features need them. - Migrate incrementally. One feature at a time, one PR at a time. No big-bang restructures.
- Enforce with tooling. ESLint boundaries, TypeScript project references, or Nx module boundaries. Structure without enforcement decays.
The best architecture is invisible. When a developer opens your project and immediately knows where to find what they need, without reading a README or asking a teammate, your structure is doing its job.
