This repository is a proof of concept (POC) exploring how far we can push Clean Architecture and Domain-Driven Design (DDD) to keep business logic completely framework-agnostic.
The project demonstrates how a framework layer (NestJS) can be wired around a pure core that contains all business rules and use cases — with the long-term goal of reusing the same core across multiple frameworks (NestJS, Express, Fastify, Elysia, etc.).
This is an architecture-focused project. Feature completeness and UI are intentionally out of scope.
Modern backend systems often become tightly coupled to frameworks and infrastructure, making them hard to test, refactor, or migrate.
This project exists to explore whether we can:
- Treat business logic as the single source of truth
- Isolate it from frameworks, transport layers, and databases
- Swap or add frameworks without rewriting business rules
- Test business logic without bootstrapping a framework
Answer: Yes — with discipline and clear boundaries.
In this project:
-
The core layer depends only on:
- Language primitives
- Explicit interfaces (ports)
-
The core layer does not depend on:
- NestJS (or any framework)
- HTTP concepts
- ORM / database clients
- External services
All side effects (HTTP, database, auth, logging) live in adapter layers.
This makes the business logic fully testable and reusable across frameworks.
[ Framework / HTTP ]
↓
[ Adapter ]
↓
[ Core / Use Cases ]
↓
[ Ports ]
↓
[ Infrastructure / DB / Services ]
core/
├─ domain/ # Entities, Value Objects, domain rules
├─ application/ # Use cases (business logic)
├─ ports/ # Interfaces for persistence and external services
src/
├─ modules/ # Framework adapters (NestJS controllers, providers)
├─ shared/ # Utils share across modules
└─ main.ts # Application bootstrap
Dependency rule: outer layers may depend on inner layers, but never the reverse.
This repository follows strict rules to enforce boundaries:
core/must not import anything fromsrc/core/must not depend on NestJS or any framework package- Use cases accept plain data structures or input DTOs
- Adapters translate framework-specific data into core inputs
- Infrastructure implements ports defined by the core
Breaking these rules defeats the purpose of the architecture.
A core motivation behind this project is treating the business layer as a reusable package.
The core/ folder is designed so it can be:
- Extracted into its own npm package
- Versioned independently
- Shared across multiple services or frameworks
core/ # publishable package
├─ domain/
├─ application/
├─ ports/
├─ package.json
└─ tsconfig.json
Adapters then live separately:
adapter-nestjs/
adapter-fastify/
adapter-express/
adapter-elysia/
Each adapter:
- Handles framework-specific concerns
- Implements the core ports
- Wires dependencies using the framework's DI system
To support multiple frameworks:
- Controllers / routes live only in adapters
- Use cases are invoked the same way regardless of framework
- Authentication, persistence, and external services are swapped by changing implementations
This allows the same business rules to run in:
- REST APIs
- Serverless functions
- Background jobs
- Different HTTP frameworks
- Business logic tests import only
core/ - No framework bootstrap is required for use-case tests
- Adapters can be tested separately with integration tests
This keeps tests fast, focused, and reliable.
Install dependencies:
pnpm installRun the NestJS application:
pnpm infra:up
pnpm run start:devRun tests:
pnpm run testThis repository is not:
- A UI-focused application
- A performance benchmark
- A feature-complete task manager
It is intentionally scoped as an architecture experiment.
- Extract
core/into a standalone npm package - Add additional adapters (Fastify, Express, Elysia)
- Add CI for core package build & publish
- Document migration between frameworks using the same core
Teeradach (Tum) Prichasongsaenglap Backend-focused Full-stack Developer
Inspired by Clean Architecture, DDD, and Ports & Adapters.
MIT