Building a Multi-Vendor Marketplace with NestJS
Lessons from architecting a 35+ module marketplace and logistics platform — modular boundaries, real-time features, and keeping it maintainable.
A marketplace looks simple from the outside: buyers, sellers, products, orders. Under the hood, it fans out fast — providers, listings, carts, coupons, reviews, logistics, shipments, chat, moderation, notifications. Here's how I kept a 35+ module NestJS backend from turning into a big ball of mud.
Modules are boundaries, not folders
The temptation is to group code by type (controllers/, services/, entities/). That scales
badly. Instead, each domain owns its full vertical slice:
// orders/orders.module.ts
@Module({
imports: [TypeOrmModule.forFeature([Order, OrderItem]), PaymentsModule],
controllers: [OrdersController],
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {}When a module only exports its service, the dependency graph stays explicit and you can reason about blast radius before you touch anything.
Push real-time to the edges
In-app chat (Ably) and audio/video calls (Agora) don't belong in your request/response core. Treat them as integrations behind a thin service so the rest of the app depends on an interface, not a vendor.
Migrations are part of the design
With TypeORM across a core and a logistics database, versioned migrations are the contract between code and data. Generate them, review them, and never let the schema drift.
The result: a platform that's still understandable a year in — which is the only metric that matters for maintainability.