Layered architecture (routes → controllers → services → data), feature folders vs layer folders, the service layer pattern, and dependency injection basics.
Module P-1 — Application Architecture and Project Structure
What this module covers: The Blog API in F-8 worked, but it had a problem: route handlers were doing everything — validating input, querying the database, and formatting responses. At fifty routes that becomes unmaintainable. This module introduces the layered architecture pattern that separates concerns cleanly, explains why each layer exists, and gives you a project structure that scales to hundreds of endpoints without becoming a mess. Every production Node.js codebase uses some version of this.
Why Architecture Matters
Consider a route handler that does everything:
This is 40 lines doing six different jobs. Problems:
- Untestable in isolation — to test the "banned user" rule you must mock HTTP, the database, email, and inventory
- Impossible to reuse — if a mobile app and a webhook both need to place orders, this logic is duplicated or copy-pasted
- Opaque — reading it you cannot tell where HTTP ends and business logic begins
The solution is layers.
The Four-Layer Architecture
Each layer has one job and knows only about the layer below it. A service never reads req.body. A route handler never writes SQL.
Project Structure
This is the feature-based slice vs layer-based choice. The structure above groups by layer (all services together). For large apps you may prefer grouping by feature (users/users.route.js, users/users.service.js, etc.). Either works — the important thing is the layer separation, not the folder names.
Layer 1: Routes
Routes are pure wiring. They register a URL pattern, apply middleware, and delegate to a controller. Nothing else.
No logic. No database calls. Just: middleware chain → controller.
Layer 2: Controllers
Controllers handle the HTTP boundary. They read from req, call services, and write to res. They contain no business logic — they orchestrate.
Notice: the controller knows about req, res, and next. The service knows nothing about them.
Layer 3: Services
Services contain business logic. They are pure JavaScript functions — no HTTP, no req, no res. This makes them trivially testable and reusable.
This service can be called from an HTTP handler, a cron job, a CLI script, or a test — it does not care.
Layer 4: Repositories
Repositories are the only layer that talks to the database. They take plain arguments and return plain data objects. No business logic — just queries.
If you switch from Prisma to raw SQL, you change only this file. Routes, controllers, and services are untouched.
Custom Error Classes
Instead of res.status(404).json(...) scattered everywhere, throw typed errors from services and catch them in a central handler.
Now services throw new NotFoundError('Order') and the error handler maps it to 404. No HTTP code knowledge needed in services.
Dependency Injection Basics
The layers above are coupled — ordersService directly imports ordersRepo. This is fine for most applications. But for testing, you may want to inject the repository as a dependency so you can swap it for a mock.
Simple factory pattern:
This is optional at first. Start with direct imports and refactor to DI if testing becomes painful.
Applying This to the Blog API
Refactoring F-8's Blog API to the layered structure:
Each file has one job. Each file is independently testable. Adding a new feature means adding files in each layer, not modifying existing ones.
Feature Folders vs Layer Folders
The structure above groups by layer. For larger applications, grouping by feature can be better:
This co-locates everything related to one domain. Adding a new feature is adding one folder. The trade-off: slightly harder to enforce layer boundaries (nothing stops a service file from importing a controller in the same folder). Use whichever makes navigation faster for your team.
Summary
- Routes wire URLs to controllers. No logic, no database calls.
- Controllers handle HTTP. Read
req, call services, writeres. No business logic. - Services contain business logic. Pure JavaScript functions. No HTTP. Testable in isolation.
- Repositories contain database access. Only layer that calls Prisma or
pool.query. Swappable. - Custom error classes allow services to throw typed errors that the global handler maps to HTTP status codes.
- Dependency injection is optional for simple apps, valuable for testing. Factory pattern works without a DI container.
- The exact folder names do not matter. The layer boundaries do.
Next: authentication — JWT tokens, bcrypt password hashing, and protecting your routes so only the right users can access the right resources.
In a standard four-layer architecture, what is the primary responsibility of the Service layer?
Why is it recommended to throw custom error classes (like NotFoundError or ValidationError) from the Service layer instead of using res.status().json()?
When comparing "layer-based" structure (e.g., all controllers in one folder) to "feature-based" structure (e.g., all user files in a /users folder), what is a key advantage of the feature-based structure for large applications?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion