A reusable Express TypeScript backend template with multi-tenant SaaS architecture.
The included modules (accounts, auth, subscriptions, billing, etc.) act as an example setup you can keep, extend, or replace with your own domains.
src/
├── app.ts # Express app configuration (security, rate limiting, routes)
├── server.ts # Server entry point & graceful shutdown
├── config/ # Configuration with environment validation
│ ├── index.ts # Zod-based env validation + app config
│ └── swagger.ts # Swagger/OpenAPI setup
├── controllers/ # Request handlers (extends BaseController)
│ ├── BaseController.ts
│ ├── auth.controller.ts
│ ├── account.controller.ts
│ ├── subscription.controller.ts
│ ├── billing.controller.ts
│ └── ...
├── services/ # Business logic (extends BaseService)
│ ├── BaseService.ts
│ ├── auth.service.ts
│ ├── account.service.ts
│ ├── subscription.service.ts
│ └── ...
├── repositories/ # Data access layer (Repository Pattern)
│ ├── BaseRepository.ts
│ ├── AccountRepository.ts
│ ├── UserRepository.ts
│ └── ...
├── models/ # Data models/interfaces
│ ├── auth.model.ts
│ ├── account.model.ts
│ ├── subscription.model.ts
│ └── ...
├── dto/ # Data Transfer Objects
├── validators/ # Zod validation schemas
├── middlewares/ # Express middlewares (auth, validation, rate limiting)
├── errors/ # Custom error classes
├── interfaces/ # Shared interfaces (repos, services, providers)
├── di/ # Dependency Injection container & wiring
├── utils/ # Utility functions (logging, JWT, password hashing, etc.)
├── providers/ # Email, SMS, MFA provider abstractions
├── payment-providers/ # Stripe and payment provider abstractions
├── queue/ # BullMQ jobs (e.g., trial expiration)
├── routes/ # API routes
└── scripts/
└── scaffoldModule.ts # CLI to scaffold new modules
- Routes:
{module}.route.ts - Controllers:
{module}.controller.ts - Services:
{module}.service.ts - Models:
{module}.model.ts - Validators:
{module}.validator.ts
Each folder has an index.ts file that exports all modules for easy importing.
- Core template: configuration (
config), DI container (di), base classes (BaseController,BaseService,BaseRepository), errors, middlewares, and utilities are domain-agnostic and can be reused as-is. - Example domains: the existing modules (
accounts,auth,dashboard,subscriptions,billing, etc.) show how to build features on top of the core.
To add a new domain/module:
- Model: create
yourModule.model.tsinmodels/(interfaces + domain logic helpers). - Repository: create
YourModuleRepository.tsinrepositories/that extendsBaseRepository. - Service: create
yourModule.service.tsinservices/that extendsBaseServiceand contains business logic only. - Controller: create
yourModule.controller.tsincontrollers/that extendsBaseControllerand only coordinates HTTP <-> service calls. - Routes: add
yourModule.route.tsinroutes/that wires HTTP paths to controller methods. - DI registration: register repository, service, and controller in
di/setup.ts.
This pattern keeps business logic in services, data access in repositories, and HTTP concerns in controllers, so swapping the floorplan example for another product is straightforward.
You can quickly scaffold a new module (model + repository + service + controller + route) using the built-in script:
npm run scaffold myModuleThis will:
- Create:
models/myModule.model.tsrepositories/MyModuleRepository.tsservices/myModule.service.tscontrollers/myModule.controller.tsroutes/myModule.route.ts
- Print ready-to-paste snippets for:
- DI registrations in
di/setup.ts - Route export in
routes/index.ts - Route mount in
app.ts
- DI registrations in
- ✅ TypeScript for type safety
- ✅ Express.js web framework
- ✅ Multi-tenant SaaS architecture with Account system
- ✅ Trial system for MVP lead extraction (configurable trial length)
- ✅ Design Patterns:
- Repository Pattern (data access abstraction)
- Dependency Injection (loose coupling)
- DTO Pattern (data transfer objects)
- Base Classes (code reuse)
- Strategy Pattern (auth, payments, notifications)
- ✅ Security & hardening:
- Helmet (security headers)
- CORS (configurable, currently wide-open for template use)
- Gzip compression
- Basic input sanitization (guards against simple XSS and query injection)
- Centralized error handling
- ✅ Rate limiting:
- Global IP-based rate limiting
- Stricter rate limiting on auth routes
- ✅ Authentication:
- Password-based auth with bcrypt hashing
- Multi-strategy auth design (
password,oauth2,saml,ssohooks) - JWT-based sessions
- MFA support via pluggable TOTP provider
- ✅ Account & subscription system (free/trial/pro)
- ✅ Notifications layer:
- Pluggable email provider (default: noop logger)
- Pluggable SMS provider (default: noop logger)
- ✅ Zod validation for API requests and environment variables
- ✅ Health check & Swagger API docs
- ✅ Modular and scalable architecture
npm installnpm run devnpm run buildnpm startGET /health- Server health check
POST /api/v1/auth/register- Register a new user and create account (multi-tenant)POST /api/v1/auth/login- Login with multi-strategy auth (password + optional MFA, OAuth2/SAML/SSO hooks)POST /api/v1/auth/forgot-password- Request password reset (sends email/SMS via providers)POST /api/v1/auth/reset-password- Reset password using reset token
POST /api/v1/accounts- Create a new accountGET /api/v1/accounts/:accountId- Get account by IDPATCH /api/v1/accounts/:accountId- Update accountDELETE /api/v1/accounts/:accountId- Delete account
Copy .env.example to .env and configure:
PORT=3000
NODE_ENV=development
API_VERSION=v1
DATABASE_URL=
JWT_SECRET=
JWT_EXPIRES_IN=7d
TRIAL_DAYS=14
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
EMAIL_FROM_ADDRESS=no-reply@example.com
EMAIL_PROVIDER=noop
SMS_PROVIDER=noopNote: Environment variables are validated using Zod on application startup. Invalid configurations will prevent the server from starting.
- Helmet and compression are enabled in
app.tsby default. - Rate limits are controlled via:
RATE_LIMIT_WINDOW_MS– window size in ms (default 15 minutes).RATE_LIMIT_MAX_REQUESTS– max requests per IP per window (default 100).
- EMAIL_PROVIDER and SMS_PROVIDER are currently
noop(log-only) for local development and templating. - To integrate a real provider:
- Implement
IEmailProvider/ISmsProviderinproviders/. - Update
EmailProviderFactory/SmsProviderFactoryto return your implementation based on config.
- Implement
The backend is designed for SaaS applications with multiple tenants:
- Accounts: Each tenant has an account with a unique ID, name, subdomain, and plan
- Users: Users belong to an account and have roles (owner, admin, member)
- Registration: When a user registers, an account is automatically created and the user becomes the owner
Zod validators are used as middleware to validate request data:
import { validate } from '../middlewares';
import { registerBodySchema } from '../validators';
router.post(
'/register',
validate(registerBodySchema, 'body'),
authController.register
);Environment variables are validated on startup using Zod in src/config/index.ts. Invalid configurations will cause the application to exit with an error message.
- Registration (
POST /api/v1/auth/register):- Creates an account + owner user.
- Password is hashed with bcrypt before storage.
- Login (
POST /api/v1/auth/login):- Supports a
methodfield (password,oauth2,saml,sso– non-password strategies are extensible hooks). - For password logins:
- Required:
email,password. - Optional:
mfaCode(required ifmfaEnabledfor the user).
- Required:
- Supports a
- MFA (TOTP):
- Backed by a pluggable MFA provider (
TotpMfaProvider). - To enable MFA for a user:
- Generate a secret + otpauth URL via the MFA provider.
- Show QR code in the frontend.
- Verify a first code from the authenticator app.
- Persist
mfaEnabled = trueandmfaSecretfor the user.
- On subsequent logins, users with
mfaEnabledmust provide a validmfaCode.
- Backed by a pluggable MFA provider (
- Password reset:
POST /api/v1/auth/forgot-password– generates a secure token, stores its hash, and sends email/SMS via providers.POST /api/v1/auth/reset-password– verifies token, sets new hashed password, clears reset fields, and returns a fresh JWT.
Data access is abstracted through repositories, making it easy to switch data sources:
// Repository handles all data operations
const account = await accountRepository.findById(id);Services and controllers receive dependencies through constructor injection:
// Services are injected, not instantiated directly
constructor(private readonly accountService: AccountService) {}Data Transfer Objects control what data is exposed in API responses:
// Convert model to DTO before sending response
const accountDTO = AccountDTO.from(account);
return this.success(res, { account: accountDTO });Common functionality is shared through base classes:
BaseRepository- Common CRUD operationsBaseService- Common service operationsBaseController- Common response helpers
The application uses custom error classes for better error handling:
AppError- Base error classBadRequestError(400)UnauthorizedError(401)ForbiddenError(403)NotFoundError(404)ConflictError(409)ValidationError(422)
// In services/repositories
throw new NotFoundError('Account not found');
throw new ConflictError('Email already exists');Wraps async controller methods to automatically catch errors:
export class MyController extends BaseController {
myMethod = catchAsync(async (req: Request, res: Response) => {
// Your async code here
// Errors are automatically caught and passed to error handler
});
}Validation errors are automatically handled by the validator middleware and return a 400 status with detailed error messages.