This project is a fullstack application composed of:
- A Node.js / Express backend
- Two separate frontend applications
This document focuses exclusively on the backend refactoring. Both frontends were also refactored, but they are documented separately.
The backend was refactored incrementally to adopt Domain-Driven Design (DDD) principles combined with Clean Architecture. The goal was not academic purity, but to improve maintainability, testability, and long-term scalability, while also reflecting my current level of experience as a fullstack developer.
This backend is one of my earlier projects, created when I was still learning server-side development and architectural fundamentals. At the time, the implementation favored speed and experimentation over clear boundaries and long-term maintainability.
Before the refactor, the backend followed a more traditional layered / MVC-style structure:
- Controllers contained business rules
- Authorization checks were scattered
- Infrastructure concerns leaked into application logic
- Read models and write models were mixed
While the application worked, the architecture increasingly reflected the limitations of my earlier experience rather than my current understanding of backend design.
The refactor was driven by two main motivations:
-
Modernizing the codebase to reflect my current skill level Over time, I gained deeper experience with backend best practices such as:
- Explicit validation (e.g. Zod)
- Centralized authorization
- Centralized error handling
- Rate limiting
- Logging
- Clear separation of concerns
Refactoring allowed the project to showcase these practices in a cohesive and intentional way.
-
Learning and applying Domain-Driven Design in a familiar context Rather than starting a new project from scratch, I chose to refactor an existing and well-understood codebase. This made it easier to:
- Explore DDD concepts incrementally
- Compare the original architecture with a DDD-inspired approach
- Clearly see the tradeoffs and benefits of each style
The refactor aimed to:
- Isolate business rules from frameworks
- Make use cases explicit
- Centralize authorization and cross-cutting concerns
- Enable architectural evolution without rewriting the system
The backend now follows a layered architecture inspired by DDD:
backend/
├── domains/ # Core business logic (Entities, Value Objects, Rules)
├── application/ # Use cases (commands & queries)
├── interfaces/ # HTTP controllers, DTOs, middleware
├── infrastructure/ # DB, auth, logging, rate limits, DIDependencies always point inward:
Interfaces → Application → Domain
Infrastructure → Application → DomainThe Domain layer has no dependency on Express, databases, Passport, or external libraries.
The domain represents the business language of the system.
Examples:
ArticleCommentUserUserRole
Entities encapsulate rules and invariants, not HTTP or persistence concerns.
class Article {
constructor(
public readonly id: string,
public title: string,
public content: string,
public status: ArticleStatus,
public readonly authorId: string,
public readonly createdAt: Date
) {}
}No serialization logic, no database logic, no framework imports.
Each business action is represented as a use case.
Examples:
CreateArticleUseCaseDeleteCommentUseCaseListCommentRepliesUseCaseGetDashboardStatsUseCase
- Commands: mutate state (create, update, delete)
- Queries: return read models optimized for the UI
This allowed the introduction of read-specific views, such as:
- Articles with
commentsCount - Dashboard aggregates
without polluting domain entities.
This layer adapts the outside world to the application.
It contains:
- Express controllers
- HTTP middlewares
- Request validation
- DTO mapping
Controllers are intentionally thin:
- Parse input
- Call a use case
- Return a response
Authorization checks (e.g. roles) are handled via middleware, not inside controllers or use cases.
Infrastructure contains all technical details:
- Database repositories
- Passport JWT authentication
- Rate limiting
- Logging (Pino)
- Dependency Injection setup
Rate limiting is treated as an infrastructure concern, implemented via Express middleware and custom errors (e.g. RateLimitError).
authenticateJwtmiddleware handles identityrequireRolemiddleware handles authorization
This keeps both controllers and use cases free of HTTP-specific logic.
The system uses structured logging via pino-http.
Logs are added at strategic points:
- Middleware boundaries (auth, role checks)
- Controller entry / exit
- Use case execution start / completion
This provides clear request flow visibility without polluting domain logic.
This refactor intentionally avoids:
- Anemic domain models
- God controllers
- Framework-driven design
At the same time, it avoids over-engineering:
- No event sourcing
- No message bus
- No premature microservices
DDD concepts are applied selectively, where they provide clear value.
This refactor positions the backend for:
- Easier feature development
- Safer refactoring
- Clear separation of concerns
- Better long-term maintainability
The frontends were refactored independently and are documented separately.
For a high-level overview, refer to the main README.md.