Backend (NestJS)
Backend Architecture
All Manacore backends use NestJS 10-11 with consistent patterns for structure, validation, and error handling.
Project Structure
apps/{project}/apps/backend/├── src/│ ├── main.ts # Application entry point│ ├── app.module.ts # Root module│ ├── app.controller.ts # Health check, root routes│ ├── config/│ │ └── configuration.ts # Environment config│ ├── drizzle/│ │ ├── schema.ts # Database schema│ │ └── drizzle.module.ts # Drizzle ORM setup│ ├── {feature}/│ │ ├── {feature}.module.ts│ │ ├── {feature}.controller.ts│ │ ├── {feature}.service.ts│ │ ├── dto/│ │ │ ├── create-{feature}.dto.ts│ │ │ └── update-{feature}.dto.ts│ │ └── entities/│ │ └── {feature}.entity.ts│ └── common/│ ├── filters/│ ├── guards/│ ├── interceptors/│ └── decorators/├── drizzle/│ └── migrations/ # Generated migrations├── drizzle.config.ts # Drizzle CLI config└── package.jsonModule Pattern
Each feature is a self-contained module:
import { Module } from '@nestjs/common';import { UsersController } from './users.controller';import { UsersService } from './users.service';
@Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService], // Export for use in other modules})export class UsersModule {}Controllers
Controllers handle HTTP requests and delegate to services:
import { Controller, Get, Post, Body, Param, UseGuards, HttpCode, HttpStatus,} from '@nestjs/common';import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';import { UsersService } from './users.service';import { CreateUserDto } from './dto/create-user.dto';
@Controller('api/v1/users')@UseGuards(JwtAuthGuard)export class UsersController { constructor(private readonly usersService: UsersService) {}
@Get() findAll(@CurrentUser() user: CurrentUserData) { return this.usersService.findAllForUser(user.userId); }
@Get(':id') findOne(@Param('id') id: string) { return this.usersService.findById(id); }
@Post() @HttpCode(HttpStatus.CREATED) create(@Body() dto: CreateUserDto, @CurrentUser() user: CurrentUserData) { return this.usersService.create(dto, user.userId); }}Services
Services contain business logic and database operations:
import { Injectable, NotFoundException } from '@nestjs/common';import { Inject } from '@nestjs/common';import { DRIZZLE } from '@manacore/shared-drizzle';import { eq, and } from 'drizzle-orm';import { users } from '../drizzle/schema';
@Injectable()export class UsersService { constructor(@Inject(DRIZZLE) private db: DrizzleDB) {}
async findAllForUser(userId: string) { return this.db .select() .from(users) .where(eq(users.ownerId, userId)); }
async findById(id: string) { const [user] = await this.db .select() .from(users) .where(eq(users.id, id));
if (!user) { throw new NotFoundException(`User with ID ${id} not found`); }
return user; }
async create(data: CreateUserDto, ownerId: string) { const [user] = await this.db .insert(users) .values({ ...data, ownerId }) .returning();
return user; }}DTOs and Validation
Use class-validator for input validation:
import { IsString, IsEmail, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreateUserDto { @IsString() @MinLength(2) @MaxLength(100) name: string;
@IsEmail() email: string;
@IsOptional() @IsString() @MaxLength(500) bio?: string;}
// dto/update-user.dto.tsimport { PartialType } from '@nestjs/mapped-types';import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}Enable validation globally:
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes( new ValidationPipe({ whitelist: true, // Strip unknown properties forbidNonWhitelisted: true, // Error on unknown properties transform: true, // Auto-transform types }),);Error Handling
Use Go-style Result types for explicit error handling:
export type Result<T, E = Error> = | { ok: true; value: T } | { ok: false; error: E };
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });export const err = <E>(error: E): Result<never, E> => ({ ok: false, error });// Usage in serviceasync findById(id: string): Promise<Result<User, 'NOT_FOUND'>> { const [user] = await this.db .select() .from(users) .where(eq(users.id, id));
if (!user) { return err('NOT_FOUND'); }
return ok(user);}
// Usage in controller@Get(':id')async findOne(@Param('id') id: string) { const result = await this.usersService.findById(id);
if (!result.ok) { throw new NotFoundException('User not found'); }
return result.value;}Configuration
Use NestJS ConfigModule for environment variables:
export default () => ({ port: parseInt(process.env.PORT, 10) || 3000, database: { url: process.env.DATABASE_URL, }, auth: { url: process.env.MANA_CORE_AUTH_URL, },});
// app.module.tsimport { ConfigModule } from '@nestjs/config';import configuration from './config/configuration';
@Module({ imports: [ ConfigModule.forRoot({ load: [configuration], isGlobal: true, }), ],})export class AppModule {}Database Setup
Use Drizzle ORM with PostgreSQL:
import { Module, Global } from '@nestjs/common';import { ConfigService } from '@nestjs/config';import { drizzle } from 'drizzle-orm/postgres-js';import postgres from 'postgres';import * as schema from './schema';
export const DRIZZLE = Symbol('DRIZZLE');
@Global()@Module({ providers: [ { provide: DRIZZLE, inject: [ConfigService], useFactory: (config: ConfigService) => { const client = postgres(config.get('DATABASE_URL')); return drizzle(client, { schema }); }, }, ], exports: [DRIZZLE],})export class DrizzleModule {}API Response Format
Consistent response structure:
// Success{ "data": { ... }, "meta": { "total": 100, "page": 1, "limit": 10 }}
// Error{ "statusCode": 404, "message": "User not found", "error": "Not Found"}Health Checks
Every backend should have a health endpoint:
@Controller()export class AppController { @Get('health') health() { return { status: 'ok', timestamp: new Date().toISOString(), }; }
@Get('api/v1/health') healthApi() { return this.health(); }}