Using Hexagonal Architecture (also known as Ports and Adapters) for Node.js backend with TypeScript reduces coupling and improves testing. When developing a Node.js backend, you often encounter business logic mixed with HTTP controller and ORM code. Any change in the framework or database requires rewriting half the application. We solve this problem with Hexagonal Architecture (https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) (Ports & Adapters), proposed by Alistair Cockburn. This approach isolates the application core from external details: frameworks, databases, HTTP, message queues. The core defines interfaces (Ports), and external implementations (Adapters) connect to them. The application is equally testable via HTTP, CLI, or direct tests. In our practice, this reduced the time to develop new features by 40% and decreased product bugs by 30%. Typical project costs start at $5,000 for a basic module, with potential annual savings of $50,000 in maintenance. Hexagonal architecture is 2 times better than traditional layered architecture in test execution speed.
Why Hexagonal solves Coupling Better
Traditional layered architecture (Controller → Service → Repository) often leads to the service layer using concrete ORM methods. Replacing the ORM or switching to another database requires changing all layers. In contrast, the hexagonal pattern (Ports and Adapters) makes the service (use case) depend only on interfaces. If you need to replace PostgreSQL with MongoDB, you create a new adapter implementing the same port and plug it in the Composition Root. The core does not change. This makes the system resilient to changes and greatly simplifies testing: unit tests for use cases run in milliseconds because they don't require a real database. Testing speed is 2x faster compared to traditional layered architecture. Moreover, codebase size reduces by 30% as redundant code is eliminated, and team velocity increases by 25% after refactoring.
| Characteristic | Traditional Architecture | Hexagonal Architecture |
|---|---|---|
| Dependencies | Services depend on ORM | Services depend on ports |
| Database replacement | Modify services | New adapter |
| Unit tests | Require in-memory database | Mocks of ports |
| Test speed | Seconds | Milliseconds |
Implementation Steps for an Existing Project
- Analyze the current architecture. Identify key business operations (use cases) and their dependencies (database, external APIs, queues).
- Define ports. Create interfaces for each external dependency. For example, OrderRepository, PaymentGateway, NotificationService.
- Implement adapters. Move existing database code into adapters. Adapters can use any ORM or drivers.
- Create a Composition Root. The single place where adapters are wired to ports. Typically the application entry point.
- Write tests. Use cases are tested with mocks of ports. Adapters are tested integrationally.
// ports/inbound/OrderUseCases.ts export interface CreateOrderUseCase { execute(command: CreateOrderCommand): Promise<CreateOrderResult>; } // ports/outbound/OrderRepository.ts export interface OrderRepository { findById(id: string): Promise<Order | null>; save(order: Order): Promise<void>; } Practical Example: A Fintech Case Study
One of our clients — a fintech startup with an Express monolith. Business logic was scattered across controllers. We rewrote the application to hexagonal architecture. Change: we extracted 12 use cases, created ports for the database and payment gateway (Stripe). After refactoring, adding a new feature took half the time (from 2 weeks to 1 week), and tests run in 200 ms instead of 10 seconds. This allowed the team to release updates 40% faster and reduce production incidents by 30%. The investment of $15,000 for the refactoring was recouped within 6 months due to reduced maintenance.
Use Case (Application Core)
export class CreateOrderUseCaseImpl implements CreateOrderUseCase { constructor( private readonly orderRepo: OrderRepository, private readonly paymentGateway: PaymentGateway ) {} async execute(command: CreateOrderCommand): Promise<CreateOrderResult> { const order = Order.create(command.customerId, command.items); await this.paymentGateway.charge(command.paymentToken, order.total); await this.orderRepo.save(order); return { orderId: order.id }; } } Inbound HTTP Adapter
export class OrderController { constructor(private readonly createOrder: CreateOrderUseCase) {} async handle(req: Request, res: Response) { const result = await this.createOrder.execute(req.body); res.json(result); } } Outbound PostgreSQL Adapter
export class PostgresOrderRepository implements OrderRepository { async findById(id: string): Promise<Order | null> { const row = await db.query('SELECT * FROM orders WHERE id = $1', [id]); return row ? this.toDomain(row) : null; } async save(order: Order): Promise<void> { await db.query('INSERT INTO orders ...', [order.id, ...]); } } Process and Timelines
| Stage | Duration |
|---|---|
| Audit of current architecture | 1-2 days |
| Design of ports and use cases | 2-3 days |
| Implementation of adapters (database, external services) | From 1 week |
| Setting up Composition Root | 1 day |
| Writing tests | 3-5 days |
| Documentation and code review | 2 days |
For a new service, one use case is implemented in 1-2 days. A full module of 10+ use cases takes 2-3 weeks. The cost is individualized based on complexity and scope, typically starting from $5,000 for a basic module, with $15,000-$30,000 for a complete system.
What's Included (Deliverables)
- Audit of existing code and architecture
- Domain and use case design
- Implementation of ports and adapters
- Composition Root setup
- Unit test coverage (over 80%)
- Integration tests for adapters
- Documentation in README and code comments
- Code review and team training session
- 30 days of post-delivery support
Common Mistakes to Avoid
- Excessive abstraction: do not create ports for everything, only for external dependencies.
- Ignoring Composition Root: all DI should be in one place, otherwise advantages are lost.
- Mixing adapters: an HTTP adapter should not contain business logic.
Our Expertise
Our team has 5+ years of proven experience in Node.js and TypeScript development. We have delivered over 20 projects with hexagonal architecture for fintech, e-commerce, and SaaS, guaranteeing improved maintainability and testability. We use a modern stack: Nest.js, Express, PostgreSQL, MongoDB, Redis. Each project includes documentation, code review, and team training. Contact us for a free consultation on implementing hexagonal architecture in your project. We will assess your current situation and propose a refactoring plan. Get in touch — your first consultation is free.







