Unit 7.2 · Tutorial 1

Backend & API Implementation

Chapter 22 · Capstone Project
~3 hours Advanced Backend · APIs · Docker · Error Handling

Overview

This tutorial focuses on implementing the backend of your capstone project. You'll set up your project structure, create routing and controllers, implement service layer logic, add error handling and validation, and configure Docker for containerisation. By the end, you'll have a fully functional backend API.

Why this matters: The backend is the heart of your application. A well‑structured API with robust error handling is essential for a production‑ready system.

1. Project Setup

Start by initialising your backend project. The structure depends on your chosen framework, but the principles are the same.

// Example: Node.js/NestJS project structure src/ ├── main.ts ├── app.module.ts ├── auth/ │ ├── auth.module.ts │ ├── auth.controller.ts │ └── auth.service.ts ├── products/ │ ├── products.module.ts │ ├── products.controller.ts │ ├── products.service.ts │ └── dto/ │ └── create-product.dto.ts ├── common/ │ ├── filters/ │ │ └── http-exception.filter.ts │ └── interceptors/ │ └── transform.interceptor.ts └── config/ ├── configuration.ts └── database.config.ts

Key principles:

  • Separation of concerns: Controllers → Services → Repositories.
  • DTOs: Use Data Transfer Objects for validation.
  • Configuration: Use environment variables for configuration.

2. Routing & Controllers

Controllers handle incoming requests. Each controller focuses on a specific resource (e.g., Products, Users, Orders).

// ProductsController (NestJS example) @Controller('api/v1/products') export class ProductsController { constructor(private productsService: ProductsService) {} @Get() async findAll() { return this.productsService.findAll(); } @Get(':id') async findOne(@Param('id') id: string) { return this.productsService.findOne(+id); } @Post() async create(@Body() createProductDto: CreateProductDto) { return this.productsService.create(createProductDto); } @Put(':id') async update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) { return this.productsService.update(+id, updateProductDto); } @Delete(':id') async delete(@Param('id') id: string) { return this.productsService.delete(+id); } }
Best practice: Use RESTful conventions – plural nouns for endpoints, HTTP methods for actions.

3. Services & Business Logic

Services contain the business logic. They interact with repositories to access the database.

// ProductsService (NestJS example) @Injectable() export class ProductsService { constructor( @InjectRepository(Product) private productRepository: Repository ) {} async findAll(): Promise { return this.productRepository.find(); } async findOne(id: number): Promise { const product = await this.productRepository.findOneBy({ id }); if (!product) { throw new NotFoundException(`Product #${id} not found`); } return product; } async create(createProductDto: CreateProductDto): Promise { const product = this.productRepository.create(createProductDto); return this.productRepository.save(product); } async update(id: number, updateProductDto: UpdateProductDto): Promise { const product = await this.findOne(id); Object.assign(product, updateProductDto); return this.productRepository.save(product); } async delete(id: number): Promise { const result = await this.productRepository.delete(id); if (result.affected === 0) { throw new NotFoundException(`Product #${id} not found`); } } }

4. Error Handling & Validation

Proper error handling is critical for a production API. Implement global exception filters and validation pipes.

// Global exception filter (NestJS) @Catch() export class GlobalExceptionFilter implements ExceptionFilter { catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception instanceof HttpException ? exception.getStatus() : 500; const message = exception instanceof HttpException ? exception.getResponse() : { message: 'Internal server error' }; response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, ...(typeof message === 'object' ? message : { message }) }); } }
// DTO with validation (class-validator) export class CreateProductDto { @IsString() @MinLength(2) @MaxLength(100) name: string; @IsNumber() @Min(0) price: number; @IsString() @IsOptional() description?: string; }

5. Docker Configuration

Containerisation ensures your application runs consistently across environments.

# Dockerfile (Node.js example) FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:18-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist EXPOSE 3000 CMD ["node", "dist/main"]
# docker-compose.yml version: '3.8' services: api: build: . ports: - "3000:3000" environment: - DB_HOST=postgres - DB_PORT=5432 - DB_USER=postgres - DB_PASSWORD=postgres - DB_NAME=mydb depends_on: - postgres postgres: image: postgres:14 environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=mydb ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:alpine ports: - "6379:6379" volumes: postgres_data:

Quiz

Question 1

In a typical backend architecture, what is the role of the service layer?

  • To handle HTTP requests and responses
  • To contain business logic and interact with repositories
  • To define database schemas
  • To serve static files
Show answer
B. To contain business logic and interact with repositories.

Question 2

What is the purpose of a DTO (Data Transfer Object)?

  • To define database schemas
  • To validate incoming request data
  • To style HTML pages
  • To configure the server
Show answer
B. To validate incoming request data and define the shape of request/response bodies.

Question 3

Which file defines multi‑container Docker services?

  • Dockerfile
  • docker-compose.yml
  • package.json
  • .env
Show answer
B. docker-compose.yml.

Exercises

Exercise 1

Implement a service method for findAll that includes pagination (skip/take) and sorting.

Sample answer
async findAll(page: number = 1, limit: number = 10, sort: string = 'id'): Promise <[Product[], number]> { const skip = (page - 1) * limit; return this.productRepository.findAndCount({ skip, take: limit, order: { [sort]: 'ASC' } }); }

Exercise 2

Write a global exception filter that formats all error responses consistently with a timestamp and path field.

Sample answer
@Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); let status = 500; let message = 'Internal server error'; if (exception instanceof HttpException) { status = exception.getStatus(); message = exception.message; } response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message }); } }

Homework

Homework 1

Implement the complete backend API for your capstone project. Include all CRUD endpoints, validation DTOs, error handling, and Docker configuration.

Sample outline
  • Setup: Initialize your backend project.
  • Endpoints: All CRUD endpoints for each resource.
  • DTOs: Create and update DTOs with validation.
  • Services: Business logic with error handling.
  • Docker: Dockerfile and docker-compose.yml.

Mini‑Project

Backend Development Sprint

Implement the backend for your capstone project:

  • Set up your project structure
  • Implement all CRUD endpoints for your main entity
  • Add validation DTOs
  • Implement global error handling
  • Configure Docker for the backend
  • Write tests for your endpoints
Sample outline
  • Project: Create a new backend project
  • Database: Set up PostgreSQL with Prisma or TypeORM
  • Endpoints: Implement all endpoints for your main resource
  • Testing: Write unit and integration tests
  • Docker: Containerise the application

Tutorial Summary

You implemented a complete backend API for your capstone project. You learned how to structure your project, create controllers and services, handle errors and validation, and containerise with Docker. Your backend is now ready to be connected to the frontend.

Key takeaway: A well‑structured backend with proper error handling and validation is essential for a production‑ready application. Invest time in getting these fundamentals right.