Hexagonal architecture (ports and adapters) keeps your business rules at the center and pushes frameworks, databases, and queues to the edges. In Node.js, that means Express routes and Prisma clients never leak into domain code.
Fat controllers pretending to be adapters - Routes with 80 lines of business logic are not hexagonal. Fix: Extract a use case; leave the route at 5-10 lines.
Ports that mirror ORM shapes - save(prismaOrder) couples domain to Prisma. Fix: Map between domain entities and persistence DTOs inside the adapter only.
One giant composition root - main.ts becomes 400 lines of wiring. Fix: Per-module registerBillingModule() factories that return routers and services.
Skipping in-memory adapters - Teams only write Postgres adapters and skip fast tests. Fix: Ship InMemoryInvoiceRepository alongside the real one.
Hexagonal ceremony on a 3-route API - Three endpoints and one developer do not need four folders. Fix: Start with domain + routes; extract ports when a second adapter appears.
What is the difference between a port and an adapter?
A port is an interface your application defines (NotificationPort). An adapter is the concrete implementation that talks to the real world (SendGridNotificationAdapter, InMemoryNotificationAdapter).
Does hexagonal architecture require a class per use case?
No. A function chargeInvoice(deps, input) works if dependencies are passed explicitly. Classes help when use cases carry state or you use a DI container.
Where does Zod validation belong?
At the HTTP adapter boundary. Parse req.body into a typed DTO, then pass plain objects into the use case. Domain validation covers invariants Zod cannot express (e.g., "invoice must not be double-charged").
Can I use hexagonal layout with NestJS?
Yes. Nest providers implement ports; controllers are driving adapters. Keep domain folders free of @Injectable() if you want framework-free unit tests.
How do I share ports across modules?
Prefer module-local ports. If two modules need the same abstraction, move the port to shared/ports/ only when a second consumer exists - avoid premature shared kernels.
Should queue workers be adapters?
Yes. A BullMQ consumer is a driving adapter that deserializes a job payload and calls the same use case as your HTTP route.
How many ports per module is too many?
If every external call has its own port, you may be over-abstracting. Start with repositories and gateways you expect to swap or fake in tests.
Does this work with Prisma?
Wrap Prisma in a repository adapter. Never export PrismaClient from domain or application layers.
How do I migrate a god-service file incrementally?
Extract one use case and one port at a time. Leave legacy routes calling old code until the new path is tested, then delete the old block.
Is hexagonal the same as DDD?
Related but not identical. Hexagonal is about dependency direction; DDD adds bounded contexts, aggregates, and ubiquitous language. You can use hexagonal without full DDD.