Layered Architecture in Node.js and Express: Class-Based Design, Dependency Injection, and Best Practices
1. Introduction
Brief recap: layered architecture divides an application into clear layers (presentation, service, data access, and infrastructure) that separate concerns and make systems easier to reason about and maintain.
JavaScript is flexible, and that freedom can lead to entangled code in medium-to-large teams.
Express apps often begin as single-file servers and grow into monoliths where responsibilities blur.
A layered approach enforces boundaries, simplifies testing, and improves onboarding.
Read the conceptual foundation first: What is Layered Architecture? A Complete Flow, Principles, and Best Practices
2. Why Layered Architecture Fits Node.js & Express
Problems with unstructured Express apps
Mixed concerns in routes (DB calls, validation, business logic).
Hard-to-test modules and brittle CI.
Difficulty changing storage, e.g., migrating from MongoDB to Postgres.
Separation of concerns in JavaScript backend systems
Keep HTTP handling in presentation, business rules in services, and storage in repositories.
JavaScript's dynamic nature benefits strongly from explicit boundaries.
Comparison with MVC (brief)
MVC groups by role (Model/View/Controller); layered architecture organizes by responsibility boundaries across the stack.
For complex systems, layered architecture often gives clearer extension points and testability than vanilla MVC.
3. Core Layers in a Node.js + Express Application
| Layer | Responsibility | Example Files | What it SHOULD NOT contain |
|---|---|---|---|
| Presentation | Handle HTTP, map request/response | routes/*.js, controllers/*.js, DTOs | Business rules, DB queries |
| Service | Business logic, orchestration, transactions | services/*.js | HTTP concerns, SQL/ODM queries |
| Data Access (Repository) | Persistence operations, queries | repositories/*.js, models/* | Business rules, HTTP parsing |
| Infrastructure / Utils | Logging, email, caching, external APIs | lib/, utils/, clients/* | Business logic, direct route handling |
This table summarizes responsibilities so developers know where to add code and where not to.
4. Recommended Folder Structure for Layered Architecture in Node.js
A clean, scalable folder structure (example):

controllers/ : Map request to service calls, this should be thin and focused.routes/ : Wire endpoints, versioning, and validators (e.g., Joi/Zod).services/ : Encapsulate use-cases and transaction boundaries.repositories/ : Single place for SQL/NoSQL queries and pagination logic.
How this structure helps in large teams?
Clear ownership: teams own layers and folders.
Easier code reviews and code search.
Reduces merge conflicts by separating concerns.
5. Request Flow in Layered Architecture (Step-by-Step)
Textual flow diagram:
Controller (HTTP) 👉 Service (business logic) 👉 Repository (data access) 👉 Database
Steps:
routes receive the request and forward to acontroller .controller extracts params and calls aservice method.service coordinates operations (validation, orchestration), callingrepositories as needed.repository executes persistence logic and returns results.service applies business rules, returns domain result tocontroller .controller formats HTTP response and sends it back.
6. Minimal Layered Architecture Example: One Route, One Flow
1. Why a Minimal Example Matters
Many tutorials jump straight into large projects. That can obscure the boundaries between layers. A minimal example helps you internalize where responsibilities live before scaling the pattern across a full application.
This example is a mental model, intentionally small so each file has one clear reason to change.
2. Use Case Description (Very Simple)
Use case: Fetch a list of categories
Endpoint:
GET /categories
What happens at each layer (one sentence each)
Route: Accepts the HTTP request and forwards it to the controller (wired by the container).
Controller: Extracts pagination/query params and calls the service.
Service: Orchestrates the use case and asks the repository for active categories.
Repository: Runs the database query and returns raw records.
3. Folder Structure for This Example

info
We omit middleware, logging, and configs to keep the focus on the call chain and responsibilities.
Class-Based Layers with Explicit Dependency Injection
Layered architecture maps well to class-based design in Node.js: classes express responsibilities and contracts clearly, and constructor-based injection makes dependencies explicit and testable. Constructor injection reduces hidden coupling, simplifies unit testing, and keeps each layer focused on one responsibility.
"This example intentionally uses a class-based architecture for controllers, services, and repositories, with constructor-based dependency injection to keep layers decoupled and testable."
4. Route Layer (What Goes Here and Why)
In routes/category.routes.js
Explanation:
Routes should only wire HTTP → controller. No logic, no DB calls, no business rules. Import controllers from the composition root (container), not from raw controller factories.
info
Routes should depend on fully constructed controllers produced by the composition root, not on raw classes or lower layers.
5. Controller Layer (Thin by Design)
In controllers/category.controller.js
What the controller does:
Extracts request context (query params, headers).
Calls a service method and returns the response.
What it deliberately does NOT do:
No DB queries, no business rules, no formatting beyond HTTP concerns.
6. Service Layer (Business Logic Boundary)
In services/category.service.js
Why orchestration belongs here:
Services know use-case flows and domain rules, but not HTTP or persistence details.
7. Repository Abstraction (Core Teaching Moment)
In repositories/CategoryRepository.js
Why this file contains NO database code:
It is a contract. The abstract class defines the surface area of storage operations without tying implementation details.
How it enables storage swapping:
Services depend on the abstract methods. Replacing
MongoCategoryRepository with another implementation requires no changes to services.
8. Concrete Repository Implementation
In repositories/MongoCategoryRepository.js
Emphasis:
This is the ONLY place DB logic lives. Services call
findActive() without knowing query details.
9. Wiring the Layers Together
Conceptually:
routes reference controllers.
controllers call services.
services call an instance of CategoryRepository (concrete or injected abstraction).
Introduce a small composition root (container) responsible for wiring.
In container/category.container.js
Notes:
This is NOT a DI framework. It is a single file that composes concrete implementations into functional layers.
Keeping wiring here prevents layers from importing concrete implementations and keeps dependencies one-way.
10. Request Flow Recap (Reinforced Learning)
Flow in plain English: Route (GET /categories) 👉 Controller (listCategories) 👉 Service (listActiveCategories) 👉 Repository (findActive) 👉 Database
Each file has exactly one reason to change: routing/HTTP, request handling, business rules, or storage.
Dependency Flow Diagram (Textual)
MongoCategoryRepository ↓ CategoryService (business rules) ↓ CategoryController (HTTP mapping) ↓ Route (Express)
Dependencies always point inward: route depends on controller, controller depends on service, service depends on repository (injected).
Dependency Injection vs Dependency Versioning
Dependency injection manages runtime collaborators (which repository or service instance the code talks to at runtime).
Dependency versioning (npm / package.json) manages package versions and upgrades; it is unrelated to runtime wiring.
Layered architecture focuses on runtime dependency boundaries and explicit wiring so collaborators can be swapped without changing business logic.
7. Controller Layer in Express
Responsibilities
Map HTTP request to service calls.
Extract and sanitize input, call validation middleware or DTO mappers.
Format and send HTTP responses; handle simple input-level errors.
What should and should not be done here
Should: request parsing, sending response, invoking service methods.
Should not: database queries, long business logic, heavy transformations.
Conceptual example (no heavy code):
Controller receives POST /orders, validates shape, calls orderService.createOrder(payload) and returns 201 with created resource location.
8. Service Layer: The Heart of Business Logic
Why service layer is critical
Centralizes business rules and use-case flows.
Keeps controllers thin and repositories focused on storage.
Benefits for testing and reuse
Unit test services by mocking repositories and external clients.
Reuse services across different transport mechanisms (REST, GraphQL, CLI).
Common mistakes developers make
Putting DB logic in services (makes future migrations hard).
Letting controllers perform orchestration.
9. Data Access Layer (Repository Pattern)
Why controllers/services should not talk to DB directly
Repositories provide an abstraction so storage details are isolated.
Helps implement caching, paging, and query tuning in one place.
MongoDB / SQL neutrality
Repositories should expose expressive methods (findById, save, paginate) without exposing SQL/ODM specifics.
Implementation can change from Mongoose to Prisma to raw SQL with minimal service changes.
Benefits for future migrations
Swapping storage engines becomes doable because only repositories/* need changes.
10. Error Handling & Validation Across Layers
Where validation should live
Input validation: routes or middlewares (schema validation using Joi/Zod).
Domain validation: services (business rule checks).
Centralized error handling
Use an Express error-handling middleware to translate exceptions into HTTP responses.
Define error types (ValidationError, NotFoundError, DomainError, InfrastructureError) so middlewares/errorHandler can map them consistently.
How layered architecture simplifies this
Errors are thrown from the layer that detects them and handled centrally, keeping controllers clean.
11. Testing Benefits of Layered Architecture
Unit testing services (class-based, constructor-injected)
Prefer injecting test doubles (fakes) that extend the repository abstraction rather than mocking internal modules. Example test double:
Test wiring is straightforward because of constructor injection:
Why this is better:
Tests avoid spinning up Express or a real database, the service is exercised in isolation.
Extending the repository abstraction keeps tests explicit and readable compared to heavy module mocking.
Constructor injection makes swapping the real repo for a fake trivial and deterministic.
How this improves CI/CD reliability
Faster, isolated tests avoid brittle end-to-end setups and make CI pipelines faster and more predictable.
12. Layered Architecture vs MVC in Node.js (TABLE COMPARISON)
| Criterion | Layered Architecture | MVC |
|---|---|---|
| Flexibility | High — layers can be extended independently | Moderate — mapping tied to framework patterns |
| Scalability | High — clear boundaries for growth | Moderate — may conflate concerns as app grows |
| Testability | High — services and repos are easy to unit-test | Moderate — controllers often mix logic |
| Suitability for large systems | Excellent — teams can own layers | Good for small-to-medium apps |
13. Best Practices for Layered Architecture in Node.js
Keep controllers thin: one operation per endpoint.
One responsibility per layer: follow single-responsibility principle.
Avoid cross-layer access: services call repositories, not the other way around.
Naming conventions: UserService, UserRepository, UserController.
Use dependency injection (simple factories or DI libs) so tests can swap implementations.
Keep shared types or DTOs in a small types/ or contracts/ folder to avoid cyclic imports.
14. Common Anti-Patterns to Avoid
Fat controllers: controllers with DB calls, complex logic, and formatting.
DB logic in services: services should call repository methods, not run raw queries.
Skipping service layer: coupling controllers and repositories makes refactors expensive.
15. When NOT to Use Layered Architecture
Small scripts or utilities: too much ceremony for tiny code.
Early-stage MVPs: if you need speed over structure, start simpler—but refactor as the app grows.
Simple CRUD apps with minimal logic: a slimmed-down structure may suffice, but plan to evolve.
16. Video Walkthrough: Layered Architecture in Node.js
Video recommendation: Layered Architecture in NodeJs By Code Architecure
I explain these patterns visually in a detailed video walkthrough. If you prefer learning by example, watch the accompanying YouTube video where I walk through a simple Express app, show folder wiring, and demonstrate testing and repository swaps.
Watch the video walkthrough to see layered architecture in Node.js and Express applied step-by-step, with real code and explanations.
17. Conclusion
Layered architecture in Node.js delivers production-ready structure: clear responsibilities, easier testing, and safer evolution. For Node.js and Express applications that expect to grow beyond a few endpoints, this approach reduces technical debt and accelerates team collaboration.
