Aller au contenu principal

Backend Developer Guide

Comprehensive guide for backend developers working on the MyTV API.

Prerequisites

  • Node.js >= 20.x
  • TypeScript knowledge
  • NestJS fundamentals
  • SQL/PostgreSQL basics
  • Git workflow understanding

Development Environment Setup

1. Clone and Install

git clone https://github.com/mytv/mytelevision-api.git
cd mytelevision-api
npm install

2. Environment Configuration

cp .env.example .env
# Edit .env with your local settings

3. Start Dependencies

# Start PostgreSQL and Redis
npm run docker:up

# Verify containers
docker ps

4. Database Setup

# Apply migrations
npm run prisma:migrate:dev

# Generate Prisma client
npm run prisma:generate

# Seed test data (optional)
npm run prisma:seed

5. Start Development Server

npm run start:dev

The API runs at http://localhost:3000 with hot-reload enabled.

Project Structure

src/
├── application/ # Business logic layer
│ ├── dtos/ # Data Transfer Objects
│ └── services/ # Business services
├── domain/ # Domain layer (entities, interfaces)
├── infrastructure/ # External concerns
│ ├── config/ # Configuration modules
│ ├── database/ # Prisma setup
│ ├── cache/ # Redis service
│ ├── guards/ # Auth guards
│ └── i18n/ # Internationalization
├── presentation/ # API layer
│ ├── controllers/ # HTTP endpoints
│ └── modules/ # NestJS modules
└── shared/ # Utilities, constants

Creating a New Feature

Step 1: Define DTOs

// src/application/dtos/feature/create-feature.dto.ts
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreateFeatureDto {
@ApiProperty({ description: 'Feature name' })
@IsString()
@IsNotEmpty()
name: string;

@ApiProperty({ description: 'Feature description', required: false })
@IsOptional()
@IsString()
description?: string;
}

Step 2: Create Service

// src/application/services/feature/feature.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '@infrastructure/database/prisma';
import { CreateFeatureDto, UpdateFeatureDto } from '@application/dtos/feature';

@Injectable()
export class FeatureService {
constructor(private readonly prisma: PrismaService) {}

async findAll(pagination: PaginationDto) {
const { page = 1, limit = 20 } = pagination;
const skip = (page - 1) * limit;

const [data, total] = await Promise.all([
this.prisma.feature.findMany({
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
this.prisma.feature.count(),
]);

return {
data,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}

async findById(id: string) {
const feature = await this.prisma.feature.findUnique({
where: { id },
});

if (!feature) {
throw new NotFoundException(`Feature with ID ${id} not found`);
}

return feature;
}

async create(dto: CreateFeatureDto) {
return this.prisma.feature.create({
data: dto,
});
}

async update(id: string, dto: UpdateFeatureDto) {
await this.findById(id); // Verify exists

return this.prisma.feature.update({
where: { id },
data: dto,
});
}

async delete(id: string) {
await this.findById(id); // Verify exists

await this.prisma.feature.delete({
where: { id },
});
}
}

Step 3: Create Controller

// src/presentation/controllers/api/v2/feature.controller.ts
import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '@infrastructure/guards';
import { FeatureService } from '@application/services/feature';
import { CreateFeatureDto, UpdateFeatureDto } from '@application/dtos/feature';
import { PaginationDto } from '@shared/dtos';

@ApiTags('Features')
@Controller('api/v2/features')
export class FeatureController {
constructor(private readonly featureService: FeatureService) {}

@Get()
@ApiOperation({ summary: 'List all features' })
findAll(@Query() query: PaginationDto) {
return this.featureService.findAll(query);
}

@Get(':id')
@ApiOperation({ summary: 'Get feature by ID' })
findById(@Param('id') id: string) {
return this.featureService.findById(id);
}

@Post()
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Create a new feature' })
create(@Body() dto: CreateFeatureDto) {
return this.featureService.create(dto);
}

@Patch(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update a feature' })
update(@Param('id') id: string, @Body() dto: UpdateFeatureDto) {
return this.featureService.update(id, dto);
}

@Delete(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Delete a feature' })
delete(@Param('id') id: string) {
return this.featureService.delete(id);
}
}

Step 4: Create Module

// src/presentation/modules/feature.module.ts
import { Module } from '@nestjs/common';
import { FeatureController } from '@presentation/controllers/api/v2/feature.controller';
import { FeatureService } from '@application/services/feature/feature.service';

@Module({
controllers: [FeatureController],
providers: [FeatureService],
exports: [FeatureService],
})
export class FeatureModule {}

Step 5: Register in AppModule

// src/app.module.ts
import { FeatureModule } from '@presentation/modules/feature.module';

@Module({
imports: [
// ... existing modules
FeatureModule,
],
})
export class AppModule {}

Writing Tests

Unit Tests

// src/application/services/feature/feature.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { FeatureService } from './feature.service';
import { PrismaService } from '@infrastructure/database/prisma';

describe('FeatureService', () => {
let service: FeatureService;
let prisma: PrismaService;

const mockPrisma = {
feature: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
count: jest.fn(),
},
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
FeatureService,
{ provide: PrismaService, useValue: mockPrisma },
],
}).compile();

service = module.get<FeatureService>(FeatureService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('findAll', () => {
it('should return paginated results', async () => {
mockPrisma.feature.findMany.mockResolvedValue([
{ id: '1', name: 'Test' },
]);
mockPrisma.feature.count.mockResolvedValue(1);

const result = await service.findAll({ page: 1, limit: 20 });

expect(result.data).toHaveLength(1);
expect(result.meta.total).toBe(1);
});
});
});

E2E Tests

// test/feature.e2e-spec.ts
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Feature (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleRef.createNestApplication();
await app.init();
});

it('GET /api/v2/features should return array', () => {
return request(app.getHttpServer())
.get('/api/v2/features')
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('data');
expect(res.body).toHaveProperty('meta');
});
});

afterAll(async () => {
await app.close();
});
});

Database Operations

Prisma Migrations

# Create new migration
npm run prisma:migrate:dev -- --name add_feature_table

# Apply in production
npm run prisma:migrate:deploy

# Reset database (development only)
npx prisma migrate reset

# Open Prisma Studio
npm run prisma:studio

Schema Changes

// prisma/schema.prisma
model Feature {
id String @id @default(uuid())
name String
description String?
status Status @default(DRAFT)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@map("features")
}

Transactions

await this.prisma.$transaction(async (tx) => {
const feature = await tx.feature.create({ data: dto });
await tx.auditLog.create({
data: { action: 'CREATE', entity: 'Feature', entityId: feature.id },
});
return feature;
});

Code Quality

Running Checks

npm run lint          # ESLint
npm run format # Prettier
npm run test # Unit tests
npm run test:e2e # E2E tests
npm run test:cov # Coverage report
npm run build # TypeScript compilation

Naming Conventions

TypeConventionExample
Fileskebab-casefeature.service.ts
ClassesPascalCaseFeatureService
VariablescamelCasefeatureList
ConstantsUPPER_SNAKEMAX_LIMIT
InterfacesPascalCaseIFeatureRepository

Debugging

VS Code Configuration

{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach NestJS",
"port": 9229,
"restart": true
}
]
}
# Start with debugger
npm run start:debug

Git Workflow

# Create feature branch
git checkout develop
git pull origin develop
git checkout -b feature/backend/my-feature

# Make changes and commit
git add .
git commit -m "feat(feature): add feature CRUD endpoints"

# Push and create PR
git push -u origin feature/backend/my-feature

Commit Convention

feat(scope): add new feature
fix(scope): fix bug description
test(scope): add unit tests
docs(scope): update documentation
refactor(scope): refactor code

Common Patterns

Error Handling

import { NotFoundException, ConflictException } from '@nestjs/common';

// Not found
throw new NotFoundException('Resource not found');

// Duplicate
throw new ConflictException('Resource already exists');

Pagination

All list endpoints return standardized pagination:

{
"data": [...],
"meta": {
"total": 100,
"page": 1,
"limit": 20,
"totalPages": 5,
"hasNextPage": true,
"hasPreviousPage": false
}
}

Caching

const cacheKey = `feature:${id}`;
const cached = await this.redis.get(cacheKey);
if (cached) return JSON.parse(cached);

const result = await this.prisma.feature.findUnique({ where: { id } });
await this.redis.set(cacheKey, JSON.stringify(result), 'EX', 300);
return result;