Module P-1·22 min read

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:

javascript

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

text

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

text

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.

javascript

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.

javascript

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.

javascript

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.

javascript

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.

javascript
javascript

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:

javascript

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:

text

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:

text

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, write res. 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.


Knowledge Check

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 & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.