NestJS Vertical Slice Template: Real-World Backend Architecture Starter

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:

  1. User A reads product price: $10
  2. User B reads product price: $10
  3. User A updates price to $12
  4. 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: true in 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.


Posted

in

,

by

Tags: