Split a NestJS app the way Lahebo billing forced me to
A module per folder type looks tidy until subscriptions, auth, and invoices import each other. Follow this split on any NestJS SaaS.
Lahebo is NestJS + Vue with Stripe subscriptions. Early on I grouped every controller in one folder. Changing a billing rule meant touching five directories. This is the layout I use now.
Step 1: One module per product capability
orders, vendors, subscriptions, media. Not controllers/, services/, dto/ at the root.
import { Module } from "@nestjs/common";
import { KernelModule } from "../kernel/kernel.module";
import { SubscriptionsController } from "./subscriptions.controller";
import { SubscriptionsService } from "./subscriptions.service";
@Module({
imports: [KernelModule],
controllers: [SubscriptionsController],
providers: [SubscriptionsService],
exports: [SubscriptionsService],
})
export class SubscriptionsModule {}
Step 2: Put auth, logging, and config in a small kernel
Features import the kernel. Features do not import each other unless there is a real domain reason — same rule I would apply to WebPasal’s site-builder modules.
Step 3: Keep DTOs next to the controller that owns them
- If two features need the same write, extract a domain service, not a circular import.
- A module that needs five other modules is usually two features hiding in one name.
- Stripe webhooks live in subscriptions, not in a generic webhooks dump.
Step 4: Ban feature-to-feature imports in lint if you have to
The learning: NestJS folders are how Lahebo stays shippable after year two. Copy the rule, not the folder names.