Introduction
The NestJS Vertical Slice Template is a practical backend boilerplate for building scalable, maintainable, and operations-ready NestJS APIs.
It is not just a simple “hello world” NestJS starter. It combines real production engineering ideas: Vertical Slice Architecture, TypeORM, PostgreSQL, OpenTelemetry, Docker Compose, Swagger, API versioning, structured error handling, and different types of testing.
The repository describes itself as a practical API sample based on Vertical Slice Architecture, NestJS, TypeORM, and OpenTelemetry.
This makes it useful for engineers who want to learn how professional backend projects are structured from the beginning, not after the project becomes messy.
1. What Problem This Template Solves
Many NestJS projects start with a simple layered structure:
- controller
- service
- repository
- DTO
- entity
- module
This is fine for small projects. But when the project grows, this structure often becomes difficult to maintain.
A simple feature change may require touching many folders:
- controller folder
- service folder
- DTO folder
- entity folder
- repository folder
- shared utils
- validation
- tests
The problem is that code is grouped by technical layer, not by business feature.
Vertical Slice Architecture fixes this by grouping code around a use case.
Instead of thinking:
“Where is the controller? Where is the service? Where is the repository?”
You think:
“Where is the Create Product feature?”
That feature owns its own controller, handler, DTO, validation, and business logic.
This improves maintainability because each feature becomes easier to understand, change, and test.
2. What Is Vertical Slice Architecture?
Vertical Slice Architecture organizes an application by feature or use case, not by technical layer.
A “vertical slice” means one complete piece of functionality from the API entry point down to the database or business logic.
Example:
Create Product Slice
├── create-product.controller.ts
├── create-product.handler.ts
├── create-product.dto.ts
├── validation
├── repository call
└── response mapping
Each slice is self-contained.
The template explains that each request is treated as a distinct use case or slice. Instead of coupling code across layers, the application couples code vertically around a feature. This reduces coupling between slices and increases cohesion inside each slice.
In simple words:
Code that changes together should live together.
That is the core idea.
3. Why Vertical Slice Architecture Is Useful in NestJS
NestJS already gives a strong module system. But many teams still create large services that become too powerful and too messy.
Example of bad growth:
products.service.ts
├── createProduct()
├── updateProduct()
├── deleteProduct()
├── getProductById()
├── getProductsByPage()
├── importProducts()
├── exportProducts()
├── validateProduct()
├── calculateProductPrice()
└── many private helper functions
This becomes a “god service”.
Every developer edits the same file. Merge conflicts increase. Bugs become harder to isolate. Testing becomes heavier.
With Vertical Slice Architecture, each use case is separated:
features/
├── create-product/
│ ├── create-product.controller.ts
│ └── create-product.handler.ts
├── update-product/
│ ├── update-product.controller.ts
│ └── update-product.handler.ts
├── get-product-by-id/
│ ├── get-product-by-id.controller.ts
│ └── get-product-by-id.handler.ts
└── get-products-by-page/
├── get-products-by-page.controller.ts
└── get-products-by-page.handler.ts
This is cleaner because each feature has a clear boundary.
4. Template Main Features
The template includes several important backend engineering features.
4.1 NestJS
NestJS is the main backend framework. It provides modules, dependency injection, decorators, controllers, providers, guards, interceptors, filters, and pipes.
The template uses NestJS as the application framework for scalable server-side development.
NestJS is a strong choice for teams because it gives structure. Without structure, Node.js backend projects can become inconsistent quickly.
4.2 TypeScript
The project uses TypeScript for type safety.
TypeScript helps catch errors before runtime. For backend APIs, this is important because request bodies, response shapes, configuration values, and database entities need to stay predictable.
TypeScript does not remove all bugs, but it reduces careless mistakes.
4.3 TypeORM
The template uses TypeORM for database access.
TypeORM maps TypeScript classes to database tables. It helps manage entities, repositories, relations, migrations, and database queries.
The repo lists TypeORM and the NestJS TypeORM package as part of its technology stack.
In a production project, TypeORM should be used carefully. It is useful, but bad usage can cause performance problems, especially with lazy loading, unnecessary joins, and hidden N+1 queries.
A serious team should still understand SQL, indexes, transactions, and query plans.
4.4 PostgreSQL
The template uses PostgreSQL as the database.
PostgreSQL is a strong default choice for production systems because it supports transactions, indexing, JSONB, constraints, full-text search, extensions, and reliable relational modeling.
The README says the project uses a PostgreSQL database running in Docker Compose.
For real systems, PostgreSQL is usually a better default than jumping immediately to NoSQL.
4.5 Database Folder and Migrations
The backend structure includes a database/ folder for database and persistence-related configuration.
This matters because database logic should not be randomly mixed with business logic.
A professional backend needs clear database management:
- local data source config
- migration files
- seed files
- test database setup
- production database setup
Do not rely on synchronize: true in production. That is dangerous. Migrations should be explicit, reviewed, and committed.
4.6 OpenTelemetry
One of the strongest parts of this template is observability.
The template integrates OpenTelemetry and OpenTelemetry Collector for logs, metrics, and distributed traces.
This is important because real APIs need more than logs.
In production, you need to answer questions like:
- Which endpoint is slow?
- Which database query is causing latency?
- Which service call failed?
- What happened before the error?
- Is the issue from code, database, network, or infrastructure?
- How many requests are failing?
- What is the p95 latency?
OpenTelemetry helps collect this data in a standard way.
A serious backend should be observable from day one, not after production incidents.
4.7 Docker Compose
The template includes Docker Compose support.
The README shows Docker Compose commands for starting and stopping the local PostgreSQL infrastructure.
Example:
docker-compose -f ./deployments/docker-compose/docker-compose.yaml up -d
Docker Compose is useful because it allows developers to run dependencies locally without manually installing every service.
For a backend project, Docker Compose usually includes:
- PostgreSQL
- Redis
- RabbitMQ
- OpenTelemetry Collector
- Prometheus
- Grafana
- Jaeger or Tempo
- MinIO
- local mail service
This template starts with PostgreSQL and observability-related infrastructure.
4.8 Swagger and API Versioning
The template uses Swagger and API versioning for application APIs.
Swagger is useful because it gives interactive API documentation.
API versioning is important because public APIs change over time. Without versioning, breaking changes can damage frontend apps, mobile apps, or external integrations.
Example:
/api/v1/products
/api/v2/products
A backend should not break old clients without a migration plan.
4.9 Problem Details Standard
The template uses the Problem Details standard for readable error responses.
This is a good practice.
Bad error response:
{
"error": "Something went wrong"
}
Better error response:
{
"type": "https://example.com/errors/validation-error",
"title": "Validation failed",
"status": 400,
"detail": "Product name is required",
"instance": "/api/v1/products"
}
Structured errors help frontend developers, QA engineers, API consumers, and logs.
4.10 Testing
The template supports:
- unit tests
- integration tests
- end-to-end tests
The README states that the project implements a comprehensive test suite including unit, integration, and E2E tests.
This is important because each test type catches different problems.
Unit tests check small logic.
Integration tests check multiple parts together, often with database or external dependencies.
E2E tests check the full API behavior from request to response.
A professional backend should not only test pure functions. It should test real API behavior.
5. Backend Folder Structure
The backend is organized around application modules and vertical slices.
A simplified structure looks like this:
backend/
├── config/
│ ├── appsettings.json
│ ├── appsettings.development.json
│ ├── appsettings.production.json
│ ├── appsettings.test.json
│ └── env/
│ ├── .env.development
│ ├── .env.production
│ └── .env.test
│
├── src/
│ ├── main.ts
│ └── app/
│ ├── app.module.ts
│ ├── app.infrastructure.ts
│ └── modules/
│ ├── health/
│ ├── products/
│ │ ├── products.module.ts
│ │ ├── products.mapper.ts
│ │ ├── products.tokens.ts
│ │ ├── contracts/
│ │ ├── data/
│ │ ├── dtos/
│ │ ├── entities/
│ │ └── features/
│ │ ├── create-product/
│ │ ├── get-product-by-id/
│ │ └── get-products-by-page/
│ └── shared/
│
└── database/
The README describes the backend as organized with Vertical Slice Architecture, where each feature is a self-contained use case spanning controller, DTO, handler, and data access.
6. How a Product Feature Is Organized
The product module is a good example.
It contains:
products/
├── products.module.ts
├── products.mapper.ts
├── products.tokens.ts
├── contracts/
│ └── product-repository.ts
├── data/
│ ├── product.repository.ts
│ └── product.schema.ts
├── dtos/
│ ├── create-product-dto.ts
│ ├── get-product-dto.ts
│ └── get-products-paged-dto.ts
├── entities/
│ └── product.entity.ts
└── features/
├── create-product/
│ ├── create-product.controller.ts
│ └── create-product.handler.ts
├── get-product-by-id/
│ ├── get-product-by-id.controller.ts
│ └── get-product-by-id.handler.ts
└── get-products-by-page/
├── get-products-by-page.controller.ts
└── get-products-by-page.handler.ts
This structure separates shared module-level code from feature-specific code.
The features/ folder contains real use cases.
The contracts/ folder defines abstractions.
The data/ folder contains concrete persistence implementation.
The dtos/ folder contains API request and response shapes.
The entities/ folder contains domain/database models.
7. Request Flow Example
Imagine the API receives this request:
POST /products
The flow may look like this:
HTTP Request
↓
create-product.controller.ts
↓
create-product.handler.ts
↓
DTO validation
↓
Product repository interface
↓
TypeORM repository implementation
↓
PostgreSQL
↓
Response DTO
↓
HTTP Response
The controller should stay thin.
The handler should contain the use case logic.
The repository should hide database details.
The DTO should define API input and output shape.
This is a clean separation.
8. Why This Is Better Than a Large Service File
In a traditional NestJS project, you may have this:
products.controller.ts
products.service.ts
products.repository.ts
At first, this looks simple.
But after one year, products.service.ts may contain thousands of lines.
That is not scalable.
Vertical Slice Architecture prevents this by forcing each use case into its own folder.
Better:
create-product/
update-product/
delete-product/
get-product-by-id/
get-products-by-page/
Each feature can evolve independently.
This is better for:
- large teams
- code reviews
- testing
- debugging
- onboarding
- reducing merge conflicts
- reducing accidental side effects
9. Strong Engineering Practices in the Template
9.1 Configuration Management
The template has configuration files for different environments:
appsettings.development.json
appsettings.production.json
appsettings.test.json
.env.development
.env.production
.env.test
This is good because development, test, and production should not share the same configuration.
A bad backend often has hardcoded values.
A serious backend uses environment-based configuration.
9.2 Code Quality Tools
The template uses ESLint and Prettier for code quality and formatting.
This matters because formatting should not be a debate in code review.
Code review should focus on correctness, architecture, security, and performance, not spacing and quotes.
9.3 Husky and Commit Linting
The template includes workflow tooling such as scripts, hooks, Husky, and commit linting.
This helps enforce quality before code reaches the repository.
Example checks:
- format check
- lint check
- test check
- commit message format
This is useful for team development.
9.4 UUID v7
The template uses sortable UUID v7 for IDs.
This is a smart choice.
Classic UUID v4 is random. Random IDs can cause index fragmentation in databases.
UUID v7 is time-sortable, which can be better for database indexing and ordering.
For high-write systems, ID strategy matters.
9.5 Optimistic Concurrency
The template uses optimistic concurrency based on a TypeORM concurrency token.
Optimistic concurrency helps prevent lost updates.
Example problem:
- User A reads product price: $10
- User B reads product price: $10
- User A updates price to $12
- User B updates stock and accidentally saves old price $10 again
Without concurrency control, User B can overwrite User A’s update.
With optimistic concurrency, the system detects that the record has changed.
This is important for real business systems.
9.6 Soft Delete
The template uses soft delete based on TypeORM.
Soft delete means data is not immediately removed from the database. Instead, a deleted flag or deleted timestamp is set.
Example:
deleted_at = 2026-06-08 10:30:00
This is useful for:
- audit trails
- recovery
- business history
- avoiding accidental permanent deletion
But soft delete must be designed carefully. Queries must consistently exclude deleted records unless explicitly needed.
10. Observability Explained
Observability means the system gives enough information to understand its internal state from external outputs.
The three major observability signals are:
10.1 Logs
Logs tell you what happened.
Example:
User login failed because password was invalid
Logs are useful, but logs alone are not enough.
10.2 Metrics
Metrics tell you numbers over time.
Example:
requests_total = 50000
error_rate = 2%
p95_latency = 320ms
database_connections_active = 15
Metrics help detect system health.
10.3 Traces
Traces show the journey of one request across services and components.
Example:
POST /products
├── controller: 3ms
├── validation: 2ms
├── handler: 8ms
├── database insert: 45ms
└── total: 65ms
For distributed systems, tracing is extremely valuable.
OpenTelemetry gives a standard way to collect these signals.
11. Why OpenTelemetry Matters for Scalable APIs
When your API is small, you can debug by reading console logs.
That does not scale.
In production, you need to know:
- which endpoint is slow
- which database query is slow
- which dependency failed
- which request caused an error
- how often the error happens
- whether performance is getting worse
- whether a deployment introduced latency
OpenTelemetry helps answer these questions.
This is why the template is more ops-ready than basic NestJS starters.
12. Testing Strategy Explained
12.1 Unit Tests
Unit tests check small parts of the application.
Example:
Does CreateProductHandler reject invalid product names?
Unit tests should be fast.
They usually mock dependencies.
12.2 Integration Tests
Integration tests check multiple components working together.
Example:
Does the product repository correctly save and read from PostgreSQL?
Integration tests are slower than unit tests but more realistic.
The template uses Testcontainers, which supports tests with throwaway Docker containers.
This is a strong practice because tests run against real infrastructure instead of fake mocks.
12.3 End-to-End Tests
E2E tests check the full API flow.
Example:
POST /products returns 201 and stores the product in the database
E2E tests are the closest to real user behavior.
They are slower, but they catch problems that unit tests miss.
13. How to Run the Template Locally
The basic local flow is:
Step 1: Start Infrastructure
docker-compose -f ./deployments/docker-compose/docker-compose.yaml up -d
This starts the PostgreSQL container.
Step 2: Install Dependencies
npm run install:dependencies
The project uses pnpm as its package manager.
Step 3: Run Backend
pnpm run dev:backend
The README also includes a debug command:
pnpm run debug:backend
Step 4: Open Swagger
After running the backend, Swagger is available at:
http://localhost:5000/swagger
The README states Swagger UI is available at this address after running the project.
Step 5: Run Tests
pnpm run test:unit:backend
pnpm run test:integration:backend
pnpm run test:e2e:backend
Step 6: Build Backend
pnpm run build:backend
Step 7: Format and Lint
pnpm run format:backend
pnpm run lint:fix:backend
pnpm run lint:backend
14. Where This Template Is Strong
This template is strong because it includes many things that real backend projects need early:
- feature-based architecture
- NestJS module structure
- TypeORM integration
- PostgreSQL local setup
- Docker Compose
- OpenTelemetry
- OpenTelemetry Collector
- Swagger
- API versioning
- Problem Details error format
- testing structure
- TypeScript
- linting and formatting
- commit hooks
- UUID v7
- optimistic concurrency
- soft delete
Most beginner templates do not include this level of operational thinking.
15. Where You Still Need to Be Careful
This template is a strong starting point, but it is not a full production system by itself.
You still need to design:
15.1 Authentication and Authorization
The template structure is useful, but real projects need proper auth:
- access token
- refresh token
- RBAC or PBAC
- permission guards
- token rotation
- session invalidation
- audit logging
- secure cookie strategy
Do not treat backend architecture as complete without security.
15.2 Database Migration Discipline
You need strict migration rules:
- never use
synchronize: truein production - generate migrations carefully
- review migration SQL
- test rollback strategy
- backup before production migration
- avoid destructive changes without plan
Database mistakes are expensive.
15.3 Performance
Vertical Slice Architecture improves maintainability, not automatically performance.
You still need to monitor:
- slow queries
- missing indexes
- N+1 queries
- large payloads
- memory usage
- connection pool exhaustion
- expensive serialization
- slow external services
Architecture does not replace performance engineering.
15.4 Transaction Boundaries
Each use case should clearly define transaction boundaries.
Example:
Create Order
├── validate cart
├── create order
├── reserve stock
├── create payment intent
└── publish event
This may need a database transaction, outbox pattern, or retry design.
Do not hide transaction logic randomly inside repositories.
15.5 Shared Code Abuse
Vertical Slice Architecture can become messy if developers abuse shared folders.
Bad pattern:
shared/
├── product-utils.ts
├── order-utils.ts
├── user-utils.ts
├── random-helper.ts
└── common-service.ts
When everything becomes shared, the architecture becomes unclear again.
Shared code should be small, stable, and truly cross-cutting.
16. How I Would Use This Template in a Real Company Project
For a real company backend, I would use this template as a foundation, then add company-specific standards.
Recommended additions:
16.1 Auth Module
Add:
- login
- refresh token
- logout
- current user endpoint
- force password reset
- role and permission guards
- API client app header validation
16.2 Standard API Response
Define one consistent response format:
{
"success": true,
"message": "Product created successfully",
"data": {}
}
For list responses:
{
"success": true,
"data": [],
"pagination": {
"page": 1,
"limit": 20,
"total_rows": 100,
"total_pages": 5
}
}
16.3 Audit Logs
Add audit logging for important actions:
- login
- create user
- update role
- change permission
- create payment
- update machine configuration
- delete record
Audit logs are critical for admin systems.
16.4 Rate Limiting
Add rate limiting for:
- login
- OTP request
- password reset
- public APIs
- payment-related APIs
This protects the backend from abuse.
16.5 Health Checks
Add health checks for:
- API process
- PostgreSQL
- Redis
- RabbitMQ
- external payment gateway
- storage service
A /health endpoint is not enough if it only returns ok.
16.6 CI/CD Pipeline
Add pipeline checks:
- install dependencies
- lint
- typecheck
- unit tests
- integration tests
- build
- Docker image build
- migration validation
- deployment
No serious backend should deploy manually forever.
17. Example: How to Add a New Feature
Suppose we want to add a feature:
Create Region
With Vertical Slice Architecture, we create:
regions/
├── regions.module.ts
├── contracts/
│ └── region-repository.ts
├── data/
│ ├── region.repository.ts
│ └── region.schema.ts
├── dtos/
│ ├── create-region-dto.ts
│ └── region-response-dto.ts
├── entities/
│ └── region.entity.ts
└── features/
└── create-region/
├── create-region.controller.ts
├── create-region.handler.ts
└── create-region.spec.ts
The create-region slice owns the use case.
It should not depend on unrelated feature internals.
18. Example Feature Flow: Create Region
Request:
POST /api/v1/regions
Request body:
{
"name": "Cambodia",
"code": "KH",
"timezone": "Asia/Phnom_Penh",
"currency": "KHR"
}
Flow:
CreateRegionController
↓
CreateRegionDto validation
↓
CreateRegionHandler
↓
Check duplicate region code
↓
RegionRepository.create()
↓
PostgreSQL insert
↓
Return RegionResponseDto
This is clean because all create-region logic is easy to find.
19. Vertical Slice vs Layered Architecture
Layered Architecture
controllers/
services/
repositories/
dtos/
entities/
Pros:
- simple at the beginning
- familiar to many developers
- easy for small CRUD apps
Cons:
- large services
- weak feature ownership
- more merge conflicts
- changes spread across folders
- harder onboarding in large systems
Vertical Slice Architecture
features/
├── create-product/
├── update-product/
├── delete-product/
└── get-product-by-id/
Pros:
- feature ownership
- easier code navigation
- better maintainability
- better testing boundaries
- less accidental coupling
Cons:
- can feel verbose
- requires discipline
- shared code must be controlled
- not every small endpoint needs overengineering
The better choice depends on project size and team maturity.
For a serious business backend, Vertical Slice Architecture is usually better long-term.
20. Important Engineering Lesson
This template teaches a key lesson:
A backend project is not only about writing APIs. It is about building a system that can be developed, tested, observed, deployed, debugged, and maintained by a team.
A beginner backend usually focuses only on endpoints.
A professional backend needs:
- architecture
- testing
- observability
- database discipline
- error handling
- configuration
- deployment
- security
- documentation
- operational readiness
That is why this template is valuable.
21. Best Use Cases for This Template
This template is a good fit for:
- NestJS backend learning
- internal admin APIs
- SaaS backend
- modular monolith backend
- business management systems
- IoT platform backend
- payment-related backend
- API-first projects
- teams that want cleaner feature structure
- teams preparing for production observability
It is not ideal if you only need a tiny prototype with two endpoints.
For very small apps, this may be overkill.
22. Final Summary
The NestJS Vertical Slice Template is a strong starting point for building scalable backend APIs with NestJS.
Its biggest value is not only the code. Its biggest value is the engineering mindset.
It shows how to structure a backend around real use cases, not just technical layers.
It includes important production-oriented tools:
- NestJS
- TypeScript
- TypeORM
- PostgreSQL
- Docker Compose
- OpenTelemetry
- OpenTelemetry Collector
- Swagger
- API versioning
- Problem Details
- tests
- linting
- formatting
- commit hooks
The most important idea is this:
Build backend features as isolated, testable, observable vertical slices.
That makes the system easier to maintain as the project grows.
For real-world projects, I would still add strong authentication, authorization, audit logging, rate limiting, CI/CD, migration discipline, and production-grade monitoring.
But as a learning template and backend foundation, this is much better than a basic NestJS starter.
