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
| Type | Convention | Example |
|---|---|---|
| Files | kebab-case | feature.service.ts |
| Classes | PascalCase | FeatureService |
| Variables | camelCase | featureList |
| Constants | UPPER_SNAKE | MAX_LIMIT |
| Interfaces | PascalCase | IFeatureRepository |
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;