Hey everyone 👋
I’ve just open-sourced Create Express Auth — a production-ready Express + TypeScript boilerplate designed around Clean Architecture and Hexagonal principles.
After years of developing APIs with Node and Express, I realized most projects start fast, but scale badly. Adding features, tests, or new modules becomes painful because the core logic is tied to the framework, the ORM, or the HTTP layer. I wanted a structure that remains flexible, testable, and independent of the tools used.
🧭 Why Hexagonal Architecture (Ports & Adapters)
The Hexagonal Architecture, introduced by Alistair Cockburn in 2005, is also known as Ports and Adapters. The key insight? There’s no real difference between how a user interface and a database interact with your application — they’re both external actors that should be interchangeable.
The pattern separates your business logic from external concerns through a simple but powerful concept:
- Your Domain defines what your app does (business rules, entities, invariants)
- Your Application defines how it does it (use-cases, orchestrations)
- Your Infrastructure provides the tools to make it possible (Express, Prisma, email, DB)
Ports: Technology-Agnostic Entry Points
Think of a Port as a USB port on your computer. It defines an interface that allows foreign actors to communicate with your application, regardless of who implements it. Just as multiple devices can connect via USB adapters, multiple implementations can connect through the same Port.
Pro tip: A Port should always have at least two things connected to it — one being a test.
Adapters: Concrete Implementations
An Adapter initiates interaction with the Application through a Port using specific technology. For example:
- A REST controller is an adapter that allows HTTP clients to communicate
- A Prisma repository is an adapter that allows your app to persist data
- An email service is an adapter that allows sending notifications
You can have as many Adapters for a single Port as needed without affecting the Application core.
Two Sides of the Hexagon
Driving Side (Left) — Primary actors that initiate interaction:
- Controllers, CLI commands, event listeners
- These adapters use a Port
- The Application Service implements the Port interface
Driven Side (Right) — Secondary actors that are triggered by the Application:
- Databases, email services, message queues
- The Application Service uses a Port
- These adapters implement the Port interface
This creates Dependency Inversion: your low-level adapters (database, email) are forced to implement abstractions defined by your high-level application core.
🎯 Real Benefits You’ll Actually Feel
1. Replace Express with Fastify? No Problem.
Your business logic doesn’t know about Express. Swap the HTTP adapter, keep everything else (hope in the new release 2.0).
2. Swap Prisma for Another ORM? Minimal Impact.
The repository interface is defined in your domain. Change the implementation, not the contract.
3. Test Without Starting a Server
Your use-cases don’t depend on HTTP. Test business logic in isolation, fast.
4. Design by Purpose, Not by Technology
Your interfaces are shaped by what your business needs, not by what your framework offers. This prevents vendor lock-in and makes evolution natural.
🧱 Layers Breakdown
1. Domain — Core Business Logic
Contains the essential rules of the application. Entities, value objects, and domain-specific errors live here.
src/domain/
├── entities/
├── repositories/ # Port interfaces (abstractions)
└── errors/
This layer is framework-agnostic. It doesn’t know about Express, HTTP, or Prisma.
2. Application — Use-Cases & Services
This is where orchestration happens. Use-cases coordinate actions between repositories and services.
src/application/
└── use-cases/
Create a use-case when logic involves multiple steps, validations, or side effects (sending emails, generating tokens, etc.). For simple CRUD, controllers can call repositories directly.
3. Infrastructure — Adapters & Frameworks
Implements the interfaces (Ports) defined in the domain.
src/infrastructure/
├── http/ # Express controllers, routes, middlewares (Driving Adapters)
├── repositories/ # Prisma adapters (Driven Adapters)
└── services/ # External integrations - email, OTP (Driven Adapters)
Key principle: The domain and application layers depend only on abstractions, never on concrete frameworks.
🧪 Integration Testing Philosophy
Instead of mocking everything, integration tests spin up a real Postgres (via Docker Compose) and exercise actual routes using Supertest. This ensures your authentication flow and migrations work as expected in real conditions.
npm run docker:up
npm run test:integration
Tests reuse the same Express app factory (appFactory.ts
) without starting the server — giving full coverage of HTTP logic and persistence while keeping speed.
Because your business logic is isolated from frameworks, you can:
- Test domain logic with unit tests (fast, no dependencies)
- Test use-cases with integration tests (real DB, no HTTP)
- Test end-to-end flows with API tests (complete stack)
Each layer gets the right type of test, maximizing coverage while minimizing brittleness.
✨ Why It’s Different
Most Node boilerplates are either:
- Too minimal, forcing you to refactor everything when you scale
- Too heavy, adding unnecessary abstractions or frameworks
- Poorly structured, mixing business logic with framework code
Create Express Auth is different because it’s:
✅ Educational — shows why to use Hexagonal Architecture, not just how
⚙️ Extensible — built for customization (swap Express, swap ORM, extend domain)
🧪 Testable — proper separation makes testing natural, not painful
🔒 Secure — includes authentication, 2FA, and email verification out of the box
🎯 Pragmatic — uses the right level of abstraction, not over-engineered
⚡ Quick Start
npx create-express-auth my-app
cd my-app && docker-compose up -d
Then visit:
🧩 Stack Overview
- TypeScript — type safety and maintainability
- Express.js — lightweight, flexible web framework
- Prisma — modern ORM for PostgreSQL
- Jest + Supertest — testing made simple
- Docker — infrastructure isolation
- MailHog — fake SMTP server for dev/testing
⚠️ When NOT to Use This Pattern
Let’s be honest: Hexagonal Architecture involves complexity. It’s not the silver bullet for all applications.
Skip it if:
- You’re building a quick prototype or POC
- Your app is extremely simple (basic CRUD, no business logic)
- Your team isn’t comfortable with abstraction layers
- You need to ship yesterday
Use it if:
- Your business logic is complex and will evolve
- You need to swap infrastructure components (different DBs, frameworks)
- You want long-term maintainability over quick wins
- You value testability and can invest upfront
When properly implemented and paired with Domain-Driven Design, Ports and Adapters ensure an application’s long-term stability and extensibility. But if broken windows are allowed (logic leakage between layers), it will cause headaches.
💬 Final Thoughts
This boilerplate isn’t about writing less code — it’s about writing code that lasts.
I built this after hitting the same wall repeatedly: projects that started clean but became unmaintainable spaghetti after a few months. Hexagonal Architecture solved this for me by making the right things easy and the wrong things hard.
Create Express Auth is opinionated, but intentionally flexible. The architecture guides you, but doesn’t lock you in. Whether you’re building a startup MVP or a complex enterprise API, the principles remain the same: keep your business logic pure, depend on abstractions, and let infrastructure be replaceable.
🙏 I’d Love Your Feedback
I’m actively improving this project and would love to hear from you:
- 💭 Have you used Hexagonal Architecture in production? What challenges did you face?
- 🤔 Do you think this approach is overkill for small projects? Or is it worth the upfront investment?
- 🔧 What features would make this boilerplate more useful? (GraphQL support? Microservices templates? Different auth strategies?)
- 📚 Would you be interested in a deeper dive on specific aspects? (Testing strategies, domain modeling, migration paths from monoliths?)
Drop a comment below or open an issue on GitHub — I read everything and I’m here to discuss! 🚀
Try it out:
If you found this helpful, give it a ⭐ on GitHub — it helps others discover it too!
📚 Further Reading
Want to dive deeper into these concepts?