Code Architecture

Reading Time 5 mins

Updated on Sat, 10 Jan 2026

Related Content

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.

Why Node.js + Express needs structure as apps scale:

  • 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

LayerResponsibilityExample FilesWhat it SHOULD NOT contain
PresentationHandle HTTP, map request/responseroutes/*.js, controllers/*.js, DTOsBusiness rules, DB queries
ServiceBusiness logic, orchestration, transactionsservices/*.jsHTTP concerns, SQL/ODM queries
Data Access (Repository)Persistence operations, queriesrepositories/*.js, models/*Business rules, HTTP parsing
Infrastructure / UtilsLogging, email, caching, external APIslib/, utils/, clients/*Business logic, direct route handling

This table summarizes responsibilities so developers know where to add code and where not to.

A clean, scalable folder structure (example):

This is just a img
  • 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:

  1. routes receive the request and forward to a controller.

  2. controller extracts params and calls a service method.

  3. service coordinates operations (validation, orchestration), calling repositories as needed.

  4. repository executes persistence logic and returns results.

  5. service applies business rules, returns domain result to controller.

  6. 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)

  1. Route: Accepts the HTTP request and forwards it to the controller (wired by the container).

  2. Controller: Extracts pagination/query params and calls the service.

  3. Service: Orchestrates the use case and asks the repository for active categories.

  4. Repository: Runs the database query and returns raw records.

3. Folder Structure for This Example

This is just a img

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

Loading...

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

Loading...

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

Loading...

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

Loading...

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

Loading...

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

Loading...

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:

Loading...

Test wiring is straightforward because of constructor injection:

Loading...

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)

CriterionLayered ArchitectureMVC
FlexibilityHigh — layers can be extended independentlyModerate — mapping tied to framework patterns
ScalabilityHigh — clear boundaries for growthModerate — may conflate concerns as app grows
TestabilityHigh — services and repos are easy to unit-testModerate — controllers often mix logic
Suitability for large systemsExcellent — teams can own layersGood 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.