# Design Principles Followed in TREK Server Development: A Modular NestJS Architecture

> Discover TREK server design principles: domain-driven modularity, dependency injection, and schema-first validation in NestJS. Learn about its flexible architecture.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: architecture
- Published: 2026-06-27

---

**TREK’s server architecture implements domain-driven modularity, dependency injection, and schema-first validation using NestJS, while preserving legacy-compatible error formats through a global exception filter.**

TREK is a travel management application structured as a monorepo that modernizes legacy Express patterns through a sophisticated NestJS implementation. The server development follows specific design principles that ensure type safety, testability, and backward compatibility with existing clients. These principles manifest throughout the codebase, from the domain-specific module organization in `server/src/nest/` to the shared Zod schemas consumed by both client and server through the `@trek/shared` package.

## Domain-Driven Modularity and Service Isolation

The architecture organizes code around business capabilities rather than technical layers, ensuring each domain encapsulates its own routing logic and data persistence.

### Business Domain Controllers and Services

Each business area maintains isolated modules with dedicated controllers and services. The trips domain uses [`server/src/nest/trips/trips.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/trips/trips.controller.ts) paired with [`server/src/services/tripService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/tripService.ts), while the vacations domain follows the same pattern in [`server/src/nest/vacay/vacay.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/vacay/vacay.controller.ts) and [`server/src/services/vacayService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/vacayService.ts). Authentication concerns remain similarly isolated in [`server/src/nest/auth/auth.controller.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/auth/auth.controller.ts) and [`server/src/services/authService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/authService.ts), preventing cross-domain coupling.

```typescript
// server/src/nest/trips/trips.controller.ts
import { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { ZodValidationPipe } from '../common/zod-validation.pipe';
import { createTripSchema } from '@trek/shared/src/trip/trip.schema';
import { TripService } from '../../services/tripService';

@Controller('trips')
export class TripsController {
  constructor(private readonly trips: TripService) {}

  @Post()
  @UsePipes(new ZodValidationPipe(createTripSchema))
  async create(@Body() dto: any) {
    return this.trips.createTrip(dto);
  }
}

```

## Dependency Injection and Clean Separation of Concerns

TREK leverages NestJS’s built-in dependency injection container to decouple HTTP handling from business operations, keeping controllers thin and services testable.

### Injectable Services and Constructor Injection

Services declare their injectable status using the `@Injectable()` decorator, as seen in [`server/src/services/authService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/authService.ts). Controllers receive these services through constructor injection without direct instantiation. The `TripsController` demonstrates this pattern with `constructor(private readonly trips: TripService) {}`, while the underlying `TripService` in [`server/src/services/tripService.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/services/tripService.ts) handles Prisma database interactions independently.

```typescript
// server/src/services/tripService.ts
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class TripService {
  private prisma = new PrismaClient();

  async createTrip(data: any) {
    return this.prisma.trip.create({ data });
  }
}

```

### Validation and Error Handling Pipelines

Request processing follows a pipeline pattern where dedicated pipes and filters handle cross-cutting concerns. The `ZodValidationPipe` in [`server/src/nest/common/zod-validation.pipe.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/common/zod-validation.pipe.ts) validates incoming payloads against Zod schemas, while the `TrekExceptionFilter` in [`server/src/nest/common/trek-exception.filter.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/common/trek-exception.filter.ts) manages error formatting. This keeps controllers focused solely on delegation to injected services.

## Legacy-Compatible Error Envelope

Despite the modern NestJS foundation, TREK maintains backward compatibility with the original Express application's error response format through a global exception handling strategy.

### Global Exception Filter Implementation

The `TrekExceptionFilter` intercepts all exceptions and normalizes them into the historic `{ error: … }` JSON structure. Registered globally in [`server/src/bootstrap.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/bootstrap.ts) via `app.useGlobalFilters(new TrekExceptionFilter())`, this filter ensures that validation failures, database errors, and unexpected exceptions all return the consistent error shape expected by legacy clients.

```typescript
// server/src/bootstrap.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TrekExceptionFilter } from './nest/common/trek-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new TrekExceptionFilter());
  await app.listen(3000);
}
bootstrap();

```

## Schema-First Validation and Type Safety

Validation occurs at the boundary using strict schemas defined in the shared package, ensuring runtime type safety matches compile-time expectations across the monorepo.

### Zod Schema Integration

The `ZodValidationPipe` imports `ZodType` from `@trek/shared` and validates request bodies against schemas like `createTripSchema` before they reach service methods. This schema-first approach, combined with the monorepo's shared TypeScript definitions in the `shared/src/` directory, eliminates type drift between client forms and server APIs.

## Security Middleware and Idempotency

Custom Express-style middlewares provide defense-in-depth for security and request integrity, integrating with NestJS guards to enforce policies before route handlers execute.

### SSRF Protection and Request Deduplication

The `ssrfGuard` middleware in [`server/src/middleware/ssrfGuard.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/middleware/ssrfGuard.ts) inspects user-provided URLs to prevent Server-Side Request Forgery attacks, rejecting external URLs with a 400 status and `{ error: 'External URLs are not allowed' }` response. The idempotency middleware, tested in [`server/src/middleware/idempotency.test.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/middleware/idempotency.test.ts), guards against duplicate request processing using custom logic to track request signatures.

```typescript
// server/src/middleware/ssrfGuard.ts
import { Request, Response, NextFunction } from 'express';
import { isExternalUrl } from '../utils/url';

export function ssrfGuard(req: Request, res: Response, next: NextFunction) {
  const url = req.body?.url;
  if (url && isExternalUrl(url)) {
    return res.status(400).json({ error: 'External URLs are not allowed' });
  }
  next();
}

```

## Centralized Configuration and Background Processing

Environment management and scheduled tasks follow explicit, injectable patterns that keep secrets out of business logic.

### ConfigModule for Environment Management

Rather than scattering `process.env` accesses, TREK uses [`server/src/nest/config/config.module.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/config/config.module.ts) to load and validate configuration once, injecting values into services via NestJS's configuration provider. This centralizes environment variable handling and enables validation of required variables at application startup.

### Cron-Based Job Scheduling

The [`server/src/scheduler.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/scheduler.ts) file implements a lightweight task runner using cron expressions derived from settings. This handles periodic maintenance tasks like automatic backups without blocking the main HTTP event loop, reading configuration values from the centralized `ConfigModule` to determine execution intervals.

## Summary

- **Domain-driven modularity** organizes code by business capability (trips, vacations, auth) with dedicated controllers in `server/src/nest/` and services in `server/src/services/`.
- **Dependency injection** via `@Injectable()` decorators and constructor injection keeps controllers thin and services isolated from HTTP concerns.
- **Schema-first validation** using Zod schemas from `@trek/shared` ensures type-safe inputs across the monorepo through the `ZodValidationPipe`.
- **Legacy-compatible error handling** through `TrekExceptionFilter` maintains the `{ error: … }` response format from the original Express implementation.
- **Security middleware** including `ssrfGuard` and idempotency checks in `server/src/middleware/` protect against common attack vectors.
- **Centralized configuration** in [`config.module.ts`](https://github.com/mauriceboe/TREK/blob/main/config.module.ts) and background jobs in [`scheduler.ts`](https://github.com/mauriceboe/TREK/blob/main/scheduler.ts) complete the layered, testable architecture.

## Frequently Asked Questions

### What architectural pattern does TREK server use?

TREK follows a **layered, domain-driven architecture** built on NestJS. The codebase separates concerns into distinct layers: controllers handling HTTP requests in `server/src/nest/*/`, services containing business logic in `server/src/services/`, pipes managing validation, and filters processing exceptions. This modularity allows each domain to evolve independently while sharing common infrastructure like the `ZodValidationPipe` and `TrekExceptionFilter`.

### How does TREK maintain backward compatibility during its NestJS migration?

TREK preserves compatibility through the **`TrekExceptionFilter`** in [`server/src/nest/common/trek-exception.filter.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/common/trek-exception.filter.ts), which catches all exceptions and formats them into the `{ error: … }` JSON structure used by the legacy Express application. This global filter, registered in [`server/src/bootstrap.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/bootstrap.ts), ensures that client error handling code continues to function unchanged despite the underlying framework switch.

### Why does TREK use Zod for validation instead of NestJS's built-in ValidationPipe?

TREK implements a **schema-first validation strategy** using Zod because it shares these schemas between client and server through the `@trek/shared` package. The custom `ZodValidationPipe` in [`server/src/nest/common/zod-validation.pipe.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/common/zod-validation.pipe.ts) validates requests against these shared schemas, guaranteeing end-to-end type safety and preventing validation logic from diverging between frontend forms and backend APIs.

### Where are background tasks handled in the TREK server?

Background processing occurs in **[`server/src/scheduler.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/scheduler.ts)**, which runs cron-based jobs for tasks like automatic backups. This keeps the main HTTP event loop unblocked while ensuring periodic maintenance executes reliably. The scheduler reads configuration values from the centralized `ConfigModule` in [`server/src/nest/config/config.module.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/config/config.module.ts) to determine execution intervals.