Feature-First vs Layered Project Structure (What I Actually Use in Real Projects)
When projects get bigger, folder structure suddenly matters. I've used both the classic layered structure and feature-first. Here's when each one actually makes sense.
At the start, structure doesn’t matter. You just want the thing to run. One file, one route, ship it.
Once you have multiple features, multiple developers, real users, your folders start turning into chaos. That’s usually the exact moment you have the least time to fix it properly.
flowchart LR
A[Project] --> B[Layered]
A --> C[Feature-first]
B --> D[Controllers]
B --> E[Services]
C --> F[Authentication]
C --> G[Projects]The Classic Layered Structure
Every tutorial teaches this one first, group by type:
project/ controllers/ services/ models/ repositories/Predictable for small projects. Need a service, go to services/. Nothing to think about, the folder name tells you where to look.
But work on one feature, say tickets, and you’re jumping across four folders for one change:
controllers/ticketController.ts → services/ticketService.ts → repositories/ticketRepository.ts → models/ticket.tsFeature-First Structure
Group by feature instead of type:
project/ features/ auth/ auth.controller.ts auth.service.ts auth.model.ts tickets/ ticket.controller.ts ticket.service.ts ticket.model.ts shared/ database/ middleware/Working on tickets? Everything’s in features/tickets/. No jumping, no remembering four file locations. With multiple developers, one owns auth/, another owns tickets/, fewer merge conflicts almost automatically.
You also start thinking in features instead of technical layers, matching how product work actually gets scoped. Nobody says “let’s ship a new service layer.” They say “let’s ship the tickets feature.”
The tradeoff: “shared” becomes a judgment call. I keep shared/ for things that are genuinely infrastructure, database clients, auth middleware, logging, not domain logic that happens to be reused.
What I Actually Do
Solo project or something small, plain layered. Fast, no overthinking, no premature structure for a codebase that might not exist in a month:
app/ models/ services/ routes/Team project, or anything past a handful of features, feature-first, with a core/ folder for shared stuff:
src/ features/ tickets/ models/ services/ routes/ core/ database/ auth/The Migration Path Nobody Talks About
Going from layered to feature-first on a live codebase isn’t a find-and-replace job. The real work is untangling implicit coupling the layered structure hid from you. A ticketService.ts sitting next to authService.ts looks like a peer, move it and you notice it quietly imports from authService for no conceptual reason.
That’s the real value of the migration, not the rename but what it forces you to notice. Every import crossing a new feature boundary is a coupling you couldn’t see before. Some belong in core/, some are accidental, and the migration is where you find out which.
The Honest Reality
There’s no “perfect” structure, anyone claiming otherwise is usually selling something. It depends on team size, complexity, and how much you care that particular day. A CRUD app for a class project doesn’t need enterprise architecture.
Small project, keep it simple. Team project, feature-first, almost every time. No need to overthink it further.
Folder structure alone won’t save bad code. But it decides how fast your future self can find the thing they’re looking for when something breaks. Even if that future reader is just you, three months from now.
Conversation
Discussion
Comments will appear here after GitHub Discussions and Giscus are configured for this repository.