Skip to content

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.json

Module Pattern

Each feature is a self-contained module:

users/users.module.ts
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:

dto/create-user.dto.ts
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.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}

Enable validation globally:

main.ts
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:

common/result.ts
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 service
async 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:

config/configuration.ts
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.ts
import { 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:

drizzle/drizzle.module.ts
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();
}
}

Best Practices