Microservice Boilerplate

Modular Monolith Boilerplate

Node.js + TypeScript. A monolith today, a set of independently deployable microservices tomorrow — same codebase, controlled by one environment variable.

TypeScript Express routing-controllers TypeDI Drizzle · PostgreSQL Mongoose · MongoDB Redis JWT + OAuth Winston

About

This project is a batteries-included starting point for backend services: decorator-based routing, dependency injection, and a shared CRUD contract over two datastore drivers, wired together by convention rather than manual registration.

The goal is to let a team start as a single deployable monolith, then peel off any module (Auth, User, Post, …) into its own service later by changing the MODULES environment variable — no rewrite required, because controllers, services, and repositories are already isolated per module.

Why This Framework

Monolith-to-microservices path

Deploy as one service today; split any module into its own deployable service later via the MODULES environment variable, without rewriting code.

Convention-based wiring

Controllers, services, and repositories are discovered by file naming and folder location — no manual route registration or DI configuration.

Two datastores, one contract

PostgreSQL (Drizzle) and MongoDB (Mongoose) repositories both implement the same BaseInterface, so a module picks whichever store fits without changing its service layer.

Security defaults included

Helmet headers, a CORS allow-list, tiered rate limiting, JWT auth, and request validation are configured at the framework level, not left to each module to add.

Full TypeScript

Typed entities, repositories, and request DTOs throughout, checked at compile time rather than discovered at runtime.

Code generation

pnpm make module <Name> scaffolds a complete module — controller, service, repository, interface, and schema — matching the existing structure.

Where This Fits

Teams starting as a single service

Get authentication, user management, rate limiting, and structured logging running without first deciding on a microservices topology.

Organizations standardizing module structure

Every module follows the same controller → service → repository shape, so onboarding to module five looks the same as module one.

Systems needing relational and document data

Entities requiring strong relational constraints (users, auth) alongside entities needing flexible schemas (posts, activity data), in the same application.

Incremental service extraction

Move a single module to its own deployment (e.g. MODULES=auth) once it needs independent scaling, without restructuring the rest of the codebase.

Design & Request Flow

Every request flows through the same layered pipeline, regardless of which module handles it. Controllers never talk to the database directly — they call a Service, which depends on a Repository interface, resolved at runtime by dependency injection.

ClientHTTP request
MiddlewareHelmet · CORS · Rate Limit · Validation
Controller
cache read / write
Redis Cache
Service
Repositoryinterface
DrizzleBaseRepository
PostgreSQL
MongooseBaseRepository
MongoDB

A request passes through middleware to a Controller, which may read or write Redis before delegating to a Service and Repository. The Repository resolves to whichever store — Postgres or MongoDB — that module is configured for.

ClientHTTP request
Express middlewareHelmet · CORS · global + auth rate limits · body parser · execution-time logger
routing-controllers@Controller / @Get / @Post · class-validator on request DTOs
ControllerApp/Modules/<Name>/Controllers — HTTP concerns only
ServiceApp/Modules/<Name>/Services — business logic, injects repo by token
Repository (interface)TypeDI token "<Name>Interface" — swappable implementation
DrizzleBaseRepository / MongooseBaseRepositoryfind · create · update · delete · findAll · count · exists
PostgreSQL / MongoDB / RedisPer-module store choice
Auto-loading

ControllerLoader

Recursively scans App/Modules/<Module>/Controllers/*Controller.ts, resolves the exported class from each file, and builds a manifest module cached under .cache/controller/. Cache is invalidated by comparing file mtimes, so new controllers are picked up automatically. The same loader (base path Utils/GlobalMiddlewares/, suffix Middleware) auto-loads global middlewares.

Auto-loading

ModuleServiceProvider

Scans App/Modules/<Module>/Repositories/*Repository.ts and registers each class in the TypeDI container as a lazy factory, keyed by token <Name>Interface (e.g. UserRepository"UserInterface"). Services depend on the string token, not the concrete class.

Data layer

Dual persistence, one contract

BaseInterface<T> defines the CRUD contract every repository implements. DrizzleBaseRepository (Postgres) and MongooseBaseRepository (MongoDB) each implement it once; module repositories extend the matching base and supply only the table/model and mapping functions.

Deployment

One codebase, many services

The MODULES env var (comma-separated) restricts which modules' controllers and repositories load at boot. Leave it empty to run the full monolith, or set MODULES=auth to deploy Auth as its own microservice against the same Postgres instance.

Project Structure

Every module — User, Auth, Post, Comment — follows the same folder shape. Once you understand one module, you understand all of them.

App/
Read-only
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
App/
├── Config/           # app.ts, database.ts, security.ts — env-driven configuration
├── Bootstrap/        # Drizzle.ts, Mongoose.ts, Redis.ts, DatabaseConnection.ts, seed.ts
├── Utils/
│   ├── Core/         # ClassAutoLoader, ModuleServiceProvider, BaseController
│   │   └── database/ # BaseInterface, DrizzleBaseRepository, MongooseBaseRepository
│   ├── Common/       # Logger, ApiResponse, ExecutionTimeMiddleware
│   └── GlobalMiddlewares/  # Auto-loaded: RateLimitMiddleware, ClassValidationMiddleware
├── Modules/
│   ├── User/         # Controllers/  Services/  Schemas/  Repositories/   (Postgres)
│   ├── Auth/         # Login, JWT sessions, OAuth                        (Postgres)
│   ├── Post/         # Controllers/  Services/  Repositories/            (MongoDB)
│   ├── Comment/      # Controllers/  Services/  Repositories/            (MongoDB)
│   └── Site/         # This page — Views/ (templates) + Public/ (assets)
└── main.ts           # Bootstrap: bind repositories, load controllers, connect DB + Redis, listen

New modules are generated, not hand-wired: pnpm make module Product (Postgres) or pnpm make module Comment --db=mongodb scaffolds a Controller, Service, Repository, Interface, and Schema that all follow this same layout.

Code Preview

A read-only look at how the User module implements the layered pattern above — real source, trimmed for width. Switch files with the tabs; nothing here is editable.

Read-only
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service()
@JsonController('/users')
@UseBefore(AuthMiddleware)
export class UserController extends BaseController {
  constructor(
    @Inject() private readonly userService: UserService,
    @Inject() private readonly cacheService: CacheService
  ) { super(); }

  @Get('/')
  async list(@QueryParam('page') page = 1, @QueryParam('pageSize') pageSize = 20) {
    const cacheKey = this.cacheService.listKey('users', page, pageSize);
    const cached = await this.cacheService.get(cacheKey);
    if (cached) return this.response.paginated(cached.users, cached.total, page, pageSize);

    const { users, total } = await this.userService.list(page, pageSize);
    await this.cacheService.set(cacheKey, { users, total });
    return this.response.paginated(users, total, page, pageSize);
  }

  @Post('/')
  async create(@Body() body: CreateUserRequest, @Res() res: Response) {
    const existing = await this.userService.getByEmail(body.email);
    if (existing) return res.status(HttpStatus.CONFLICT).json(this.response.error('Email already exists'));

    const user = await this.userService.create(body);
    return res.status(HttpStatus.CREATED).json(this.response.success(user));
  }
  // ...update() and delete() follow the same controller -> service -> response shape
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service()
export class UserService {
  constructor(
    @Inject('UserInterface') private readonly userRepository: UserInterface,
    @Inject() private readonly mailService: MailService
  ) {}

  async create(data: Parameters<UserInterface['create']>[0]): Promise<User> {
    const hashed = await bcrypt.hash(data.password, 10);
    const user = await this.userRepository.create({ ...data, password: hashed });

    this.mailService
      .sendWelcomeEmail({ email: user.email, name: user.name })
      .catch((err) => logger.warn('Welcome email failed', { email: user.email, err: err.message }));

    return entityToUser(user);
  }

  async getByEmail(email: string): Promise<User | null> {
    const user = await this.userRepository.findOneBy('email', email);
    return user ? entityToUser(user) : null;
  }
  // ...update(), delete(), list() call the same repository, hashing only on password changes
}
1
2
3
4
5
6
7
8
@Service()
export class UserRepository
  extends DrizzleBaseRepository<UserEntity, UserCreateInput, UserUpdateInput>
  implements UserInterface
{
  protected readonly table = user;
  // find, create, update, delete, findAll, findOneBy, listSorted, … — inherited
}
1
2
3
4
5
6
7
8
9
10
11
export const user = pgTable('User', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  password: varchar('password', { length: 255 }).notNull(),
  name: varchar('name', { length: 255 }),
  createdAt: timestamp('createdAt', { mode: 'date' }).defaultNow().notNull(),
  updatedAt: timestamp('updatedAt', { mode: 'date' }).defaultNow().notNull(),
});

export type UserRow = typeof user.$inferSelect;
export type UserInsert = typeof user.$inferInsert;

Modules

Module Store Purpose Base repository
User PostgreSQL (Drizzle) User records, CRUD DrizzleBaseRepository
Auth PostgreSQL (Drizzle) Login/logout, JWT sessions, Google/Facebook/GitHub OAuth DrizzleBaseRepository
Post MongoDB (Mongoose) Post records, CRUD MongooseBaseRepository
Comment MongoDB (Mongoose) Comment records, CRUD MongooseBaseRepository
Site none This landing page

New modules are generated, not hand-wired: pnpm make module Product (Postgres) or pnpm make module Comment --db=mongodb scaffolds Controller/Service/Repository/Interface/Schema following this same structure.

Security

Helmet

Standard security headers; HSTS enabled in production; CSP togglable via HELMET_CSP_ENABLED.

CORS

Origins restricted via ALLOWED_ORIGIN (comma-separated, or *).

Rate limiting

Global limit via RATE_LIMIT_MAX/_WINDOW_MS; a stricter limiter (RATE_LIMIT_AUTH_MAX) applies only to /auth/login and OAuth callbacks.

JWT

Signed session tokens; startup warns in production if JWT_SECRET is under 32 characters.

Validation

Request DTOs validated with class-validator via routing-controllers' validation: true.

Body limit

BODY_PARSER_LIMIT (default 1mb) guards against oversized-payload abuse.

Getting Started

Three common ways to run this locally — switch tabs to see each.

Read-only
1
2
3
4
5
6
7
cp .env.example .env
# edit DATABASE_URL / MONGODB_URI / REDIS_URL

npm install
npm run db:push      # sync Drizzle schema to Postgres
npm run db:seed      # creates admin@example.com / Admin@123
npm run dev
1
2
3
4
5
docker-compose up -d
# API:     http://localhost:3001
# Swagger: http://localhost:3001/api-docs
# Adminer: http://localhost:8080
# Mongo Express: http://localhost:8081
1
2
3
MODULES=auth npm run dev              # Auth only, as a standalone service
MODULES=user,auth npm run dev
MODULES=post,comment npm run dev      # requires MONGODB_URI

Contact

This is a boilerplate template — replace this section with your own team's support channel (Slack, email alias, issue tracker link, etc.) before shipping it to real users.

Issues & bugs

Point this at your repository's issue tracker.

Support

Point this at your team's support inbox or chat channel.

Docs

See README.md at the project root for the full setup and environment reference.

Microservice Boilerplate — Node.js + TypeScript modular monolith. Get Started· Contact