💡 A practical api sample based on Vertical Slice Architecture, NestJs, TypeORM and OpenTelemetry.
- NestJs Vertical Slice Template
- ✅ Uses Vertical Slice Architecture for feature-based modularity and clear separation of concerns
- ✅ Implements a comprehensive test suite: Unit Tests, Integration Tests, and End-to-End (E2E) Tests
- ✅ Employs NestJS as the application framework for scalable server-side development
- ✅ Utilizes TypeORM for robust ORM and data access
- ✅ Integrates OpenTelemetry and OpenTelemetry-Collector for collecting logs, metrics, and distributed traces to enhance observability
- ✅ Enforces code quality and standards with ESLint and Prettier
- ✅ Ensures type safety and modern JavaScript with TypeScript
- ✅ Advanced backend Configuration Management based on env files and appsettings. json files
- ✅ Facilitates efficient development workflow with scripts, hooks (Husky), and commit linting
- ✅ Using Swagger and Api-Versioning for application apis
- ✅ Using Problem Details standard for readable details of errors.
- ✅ Using Docker-Compose for our deployment mechanism.
- ✅ Using sortable uuid v7 for Ids
- ✅ Using Optimistic Conurrency based on TypeORM concurrency token
- ✅ Using Soft Delete based on TypeORM
- ✅ Integration Aspire for hosting application
- ✔️
microsoft/TypeScript- TypeScript is a language for application-scale JavaScript. - ✔️
nestjs/nest- Nest is a framework for building efficient, scalable Node.js server-side applications - ✔️
nestjs/cqrs- A lightweight CQRS module for Nest framework (node.js) - ✔️
nestjs/typeorm- TypeORM module for Nest - ✔️
tada5hi/typeorm-extension- This library provides utitlites to create & drop the database, seed the database and apply URL query parameter(s) - ✔️
nestjs/swagger- OpenAPI (Swagger) module for Nest - ✔️
open-telemetry/opentelemetry-js- A framework for collecting traces, metrics, and logs from applications - ✔️
motdotla/dotenv- Dotenv is a zero-dependency module that loads environment variables from a .env - ✔️
PDMLab/http-problem-details- This library implements HTTP Problem details (RFC 7807) for HTTP APIs - ✔️
jestjs/jest- A javascript framework for testing - ✔️
testcontainers/testcontainers-node- A library to support tests with throwaway instances of Docker containers - ✔️
faker-js/faker- Generate massive amounts of fake (but realistic) data for testing and development - ✔️
florinn/typemoq- Simple mocking library for JavaScript targeting TypeScript development - ✔️
ladjs/supertest- High-level abstraction for testing HTTP - ✔️
eslint/eslint- ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code - ✔️
prettier/prettier- Opinionated Code Formatter - ✔️
uuidjs/uuid- Generate RFC-compliant UUIDs in JavaScript - ✔️
@fluffy-spoon/substitute- An NSubstitute port to TypeScript called substitute.js
This project uses a PostgreSQL database running in a Docker container. Start the infrastructure using make:
# Start docker-compose
docker-compose -f ./deployments/docker-compose/docker-compose.yaml up -d
# Stop docker-compose
docker-compose -f ./deployments/docker-compose/docker-compose.yaml downThis command will run the PostgreSQL docker container using docker-compose. Ensure Docker is installed and running on your machine.
First, we need to install pnpm because we use pnpm as our package manager. Then we should install the dependencies:
npm run install:dependencies# run backend in dev mode
pnpm run dev:backend
# run backend in debug mode
pnpm run debug:backendAfter running the project, you can access the Swagger UI at http://localhost:5000/swagger.
# run backend unit tests
pnpm run test:unit:backend
# run backend integration tests
pnpm run test:integration:backend
# run backend e2e tests
pnpm run test:e2e:backend# build backend
pnpm run build:backend# format code
pnpm run format:backend
# fix backend lints
pnpm run lint:fix:backend
# lint backend
pnpm run lint:backendInstall the Aspire CLI tool:
# Bash
dotnet tool install -g Aspire.CliTo run the application using the Aspire App Host and using Aspire dashboard in the development mode run following command:
aspire runAfter running the command, Aspire dashboard will be available with all application components.
In this project, I used vertical slice architecture and feature folder structure:
- We treat each request as a distinct use case or slice, encapsulating and grouping all concerns from front-end to back.
- When we are adding or changing a feature in an application in traditional n-tier architecture, we are typically touching many different "layers" in an application. We are changing the user interface, adding fields to models, modifying validation, and so on. Instead of coupling across a layer, we couple vertically along a slice, and each change affects only one slice.
- We
minimize couplingbetween slices andmaximize cohesionwithin a slice, ensuring that related code is grouped together logically and independently. - With this approach, each of our vertical slices can decide for itself how to best fulfill the request. New features only add code, and we're not changing shared code and worrying about side effects.
- By grouping all code related to a feature into a single slice, the architecture improves maintainability and makes it easier to understand and modify individual features without affecting others.
- Testing becomes more straightforward, as each slice encapsulates all logic related to its feature, enabling isolated and effective testing.
Our backend is organized using Vertical Slice Architecture — where each feature (use case) is a self-contained, end-to-end slice spanning controller, DTO, handler, and data access. This ensures high cohesion, low coupling, and easy maintainability.
backend/
├── 📄 .editorconfig
├── 📄 .eslintignore
├── 📄 .gitignore
├── 📄 .nycrc.json
├── 📄 .prettierignore
├── 📄 .yamllint.yml
├── 📄 eslint.config.mjs
├── 📄 jest.config.ts
├── 📄 lint-staged.config.mjs
├── 📄 nest-cli.json
├── 📄 package.json
├── 📄 pnpm-lock.yaml
├── 📄 prettier.config.js
├── 📄 README.md
├── 📄 tsconfig.build.json
├── 📄 tsconfig.json
├── 📄 tsconfig.prod.json
├── 📄 vitest.config.ts
│
├── 📁 config/
│ ├── 📄 appsettings.development.json
│ ├── 📄 appsettings.json
│ ├── 📄 appsettings.production.json
│ ├── 📄 appsettings.test.json
│ └── 📁 env/
│ ├── 📄 .env.development
│ ├── 📄 .env.production
│ └── 📄 .env.test
│
├── 📁 src/ # ✅ APPLICATION SOURCE CODE — Vertical Slices Live Here
│ ├── 📄 main.ts # ▶️ Entry point: Bootstraps NestJS application
│ │
│ └── 📁 app/
│ ├── 📄 app.module.ts # 🧩 Root module: Imports all bounded contexts (products, health, shared)
│ ├── 📄 app.infrastructure.ts # ⚙️ Global setup: Middleware, filters, pipes, interceptors (e.g., logging, CORS, error handling)
│ │
│ └── 📁 modules/
│ │
│ ├── 📁 health/
│ │ ├── 📄 health.module.ts # 🏗️ Module declaring the health endpoint — simple, no vertical slice needed
│ │ └── 📁 health/
│ │ └── 📄 health.controller.ts # 🌐 HTTP endpoint: GET /health → returns { status: 'ok' }
│ │
│ ├── 📁 products/ # 🔹 BOUNDED CONTEXT: Product Management — VERTICAL SLICES HERE
│ │ ├── 📄 products.module.ts # 🧩 Module exports controllers, handlers, repositories — context glue
│ │ ├── 📄 products.mapper.ts # 🔄 Optional: Maps DTO ↔ Entity (ClassTransformer)
│ │ ├── 📄 products.tokens.ts # 💡 DI tokens: e.g., `InjectionToken<ProductRepository>`
│ │ │
│ │ ├── 📁 contracts/ # 📜 ABSTRACTIONS — What the domain expects (interfaces)
│ │ │ └── 📄 product-repository.ts # 🖋️ Interface: find(), create(), update() — NO implementation
│ │ │
│ │ ├── 📁 data/ # 🛠️ IMPLEMENTATIONS — Concrete TypeORM repos & schemas
│ │ │ ├── 📄 product.repository.ts # 🧱 Implements `product-repository.ts` — talks to DB
│ │ │ └── 📄 product.schema.ts # 🗃️ TypeORM @Entity() schema — defines table structure
│ │ │
│ │ ├── 📁 dtos/ # 📦 DATA TRANSFER OBJECTS — API request/response shapes
│ │ │ ├── 📄 create-product-dto.ts # ✉️ Shape of POST /products body
│ │ │ ├── 📄 get-product-dto.ts # ✉️ Shape of GET /products/:id response
│ │ │ └── 📄 get-products-paged-dto.ts # ✉️ Shape of paginated list response
│ │ │
│ │ ├── 📁 entities/ # 🧬 DOMAIN MODELS — Business objects mapped to DB
│ │ │ └── 📄 product.entity.ts # 📂 TypeORM entity — represents Product in domain
│ │ │
│ │ └── 📁 features/ # ✅ VERTICAL SLICES — ONE FOLDER PER USE CASE
│ │ │
│ │ ├── 📁 create-product/ # 🎯 Use Case: Create a new product
│ │ │ ├── 📄 create-product.controller.ts # 🌐 HTTP entry point (POST /products)
│ │ │ └── 📄 create-product.handler.ts # 💼 Business logic: Validates, uses repo, returns DTO
│ │ │
│ │ ├── 📁 get-product-by-id/ # 🎯 Use Case: Fetch single product by ID
│ │ │ ├── 📄 get-product-by-id.controller.ts
│ │ │ └── 📄 get-product-by-id.handler.ts
│ │ │
│ │ └── 📁 get-products-by-page/ # 🎯 Use Case: Paginated list of products
│ │ ├── 📄 get-products-by-page.controller.ts
│ │ └── 📄 get-products-by-page.handler.ts
│ │
│ └── 📁 shared/
│ └── 📄 shared.module.ts # 🔗 Shared guards, interceptors, pipes used across contexts (e.g., AuthGuard)
│
├── 📁 database/ # 🗃️ DATABASE & PERSISTENCE LAYER (separated from business logic)
│ ├── 📄 data-source-local.config.ts # ⚙️ TypeORM config for local dev environment
│ │
│ ├── 📁 factories/ # 🧪 Test data builders (for integration tests)
│ │ └── 📄 products.factory.ts # 🧩 Creates realistic test product instances
│ │
│ ├── 📁 migrations/ # 🛠️ Database schema evolution scripts
│ │ └── 📄 1757367142854-init.ts # 📜 Generated by TypeORM — tracks DB state changes
│ │
│ └── 📁 seeds/ # 🌱 Production seed data
│ └── 📄 products-seeder.ts # 📥 Inserts default products on deployment
│
├── 📁 libs/ # 🧰 CROSS-CUTTING INFRASTRUCTURE LIBRARIES — Reusable, project-agnostic
│ ├── 📁 configurations/ # 📂 App settings loader, typed configs, env parsing
│ ├── 📁 core/ # 🧱 Base classes: entities, exceptions, validations, guards
│ ├── 📁 logger/ # 📝 Logging adapters: Nest, Pino, Winston
│ ├── 📁 opentelemetry/ # 🕵️ Distributed tracing: spans, metrics, OTLP exporters
│ ├── 📁 postgres-typeorm/ # 🐘 TypeORM + PostgreSQL integration: modules, subscribers, connection setup
│ ├── 📁 swagger/ # 📚 OpenAPI/Swagger UI configuration
│ ├── 📁 test/ # 🧪 Shared test utilities: bootstrappers, fixtures, respawners
│ ├── 📁 versioning/ # 🔢 API versioning strategy: header/path-based routing
│ └── 📁 web/ # 🌐 HTTP middleware: CORS, response time, compression, security
│
└───📁 test/
├───📁 e2e-tests/ # End-to-end API tests (HTTP level)
├───📁 integration-tests/ # Feature-level tests (with real DB/repo)
├───📁 shared/ # Common test utilities & fakes
└───📁 unit-tests/ # Pure unit tests (isolated handlers/services)-
✅ Vertical Slices First:
Every feature (create-product,get-product-by-id, etc.) is a self-contained folder underfeatures/— containing its own controller, handler, DTO, and associated tests. No sharedcontrollers/,services/, orrepositories/folders. Code is grouped by behavior, not technical role — making features independently developable, testable, and deployable. -
✅ Bounded Contexts Isolated:
Modules likeproducts/andhealth/are fully encapsulated bounded contexts. They depend only on each other through explicitcontracts/interfaces — never directly on entities, repositories, or implementations. This ensures loose coupling, clear ownership, and safe refactoring. -
✅ Infrastructure Separated & Abstracted:
Cross-cutting concerns — logging, tracing, config, DB drivers, Swagger, CORS — live exclusively inlibs/. Business logic insrc/app/modules/never imports infra directly. Instead, it depends on abstractions defined incontracts/, enabling easy swapping of implementations (e.g., switching from TypeORM to Prisma) without touching domain code. -
✅ Test-Driven by Slice:
Every vertical slice has a matching test suite:- Unit tests →
test/unit-tests/modules/[context]/features/[feature].test.ts - Integration tests →
test/integration-tests/modules/[context]/features/[feature].test.ts - E2E tests →
test/e2e-tests/modules/[context]/features/[feature].test.ts
Tests mirror the feature structure — ensuring full coverage and fast feedback loops.
- Unit tests →
-
✅ No Layered Folders:
There are no global folders likecontrollers/,services/,dtos/, orrepositories/spanning the entire app. All artifacts are scoped within their context and feature. This eliminates confusion, prevents “layer creep”, and enforces cohesion. -
✅ Reusable Infrastructure in
libs/:
Thelibs/directory contains application-agnostic, reusable infrastructure packages (configurations/,core/,postgres-typeorm/,opentelemetry/, etc.). These are treated like internal libraries — versioned, tested, and imported bysrc/modules via DI. Ensures consistency across contexts and enables reuse across microservices. -
✅ Contracts Define Boundaries:
Interfaces incontracts/are the only allowed dependency bridge between bounded contexts and infrastructure. For example,products.moduleusesproduct-repository.ts(contract), but knows nothing aboutproduct.repository.ts(implementation). This enforces inversion of control and makes mocking trivial. -
✅ Domain-Centric Entities & DTOs:
entities/represent real-world business objects (e.g.,Product) with ORM decorators — but remain pure domain models.dtos/define API contracts — strictly separate from entities. Mapping is handled explicitly inmapper.tsfiles, avoiding leakage between layers. -
✅ Database Layer Isolated:
database/contains only persistence artifacts: migrations, seeds, factories, and data source config. It is not imported by business modules — only bylibs/postgres-typeorm/and test fixtures. Ensures domain logic remains database-agnostic.
