Fundamentals Backend Security Microservices Performance Docs Database Patterns DevOps AI APIs System Design FAQ Roadmap
Backend Engineering ยท API Architecture ยท System Design

API Architect v2026

Learn to build scalable APIs, backend systems, and AI services from fundamentals to production.

Start Learning โ†’ View Roadmap
13
Modules
25+
FAQ Answers
6
Roadmap Stages

// 01 โ€” Core Concepts

API Architecture Fundamentals

REST is more than just HTTP endpoints. Understanding its constraints is the difference between an API that grows gracefully and one that becomes a maintenance nightmare.

REST Principles

REST (Representational State Transfer) is an architectural style, not a protocol. A truly RESTful API adheres to six core constraints that make it predictable, scalable, and easy to consume.

๐ŸŒ

Stateless

Each request must contain all information needed to process it. No session state is stored server-side between requests.

๐Ÿ—„๏ธ

Client-Server

Separation of concerns between UI and data storage. Client and server evolve independently.

โ™ป๏ธ

Cacheable

Responses must define themselves as cacheable or non-cacheable, reducing client-server interactions.

๐Ÿ“ฆ

Uniform Interface

Resources are identified in requests. Manipulation via representations using standard HTTP methods.

๐Ÿ”—

Layered System

Client doesn't know if it's talking to the server directly or an intermediary like a load balancer or cache.

โš™๏ธ

Code on Demand

Optional: servers can temporarily extend functionality by sending executable code to the client.

Resource-Based Endpoints

Resources are nouns, not verbs. Your URL structure should model your domain objects, not your operations.

REST โ€” Endpoint Design
// โœ… GOOD โ€” resource-based (nouns) GET /api/v1/users โ†’ list all users GET /api/v1/users/{id} โ†’ get specific user POST /api/v1/orders โ†’ create new order PUT /api/v1/products/{id} โ†’ replace product PATCH /api/v1/products/{id} โ†’ partial update DELETE /api/v1/users/{id} โ†’ delete user // โŒ BAD โ€” action-based (verbs) GET /api/getUser?id=1 POST /api/createOrder POST /api/deleteUser โ† never use POST for delete

HTTP Methods Reference

Method Usage Idempotent Body
GET Retrieve a resource or collection โœ… Yes No
POST Create a new resource โŒ No Yes
PUT Replace an entire resource โœ… Yes Yes
PATCH Partial update of a resource โŒ Usually No Yes
DELETE Remove a resource โœ… Yes Optional
HEAD Metadata only (no body) โœ… Yes No
OPTIONS CORS preflight / capabilities โœ… Yes No

HTTP Status Codes

2xx โ€” Success

200 OK ยท 201 Created ยท 204 No Content ยท 206 Partial Content

3xx โ€” Redirect

301 Moved Permanently ยท 304 Not Modified ยท 307 Temporary Redirect

4xx โ€” Client Error

400 Bad Request ยท 401 Unauthorized ยท 403 Forbidden ยท 404 Not Found ยท 422 Unprocessable Entity ยท 429 Too Many Requests

5xx โ€” Server Error

500 Internal Server Error ยท 502 Bad Gateway ยท 503 Service Unavailable ยท 504 Gateway Timeout

API Versioning Strategies

Versioning Approaches
// Option 1: URI versioning (most common, most visible) GET /api/v1/users GET /api/v2/users // Option 2: Header versioning (clean URIs) GET /api/users Accept: application/vnd.company.api+json; version=2 // Option 3: Query parameter (simple but messy) GET /api/users?version=2 // Best practice: URI versioning for public APIs, // header versioning for internal/enterprise APIs

Pagination, Filtering & Sorting

Query Parameters
// Cursor-based pagination (preferred for large datasets) GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20 // Offset-based pagination (simple, but can miss/duplicate records) GET /api/v1/users?page=3&pageSize=25 // Filtering GET /api/v1/users?status=active&role=admin&country=US // Sorting (prefix - for descending) GET /api/v1/users?sort=-createdAt,lastName // Field selection / sparse fieldsets GET /api/v1/users?fields=id,name,email

Idempotency

An operation is idempotent if calling it multiple times produces the same result as calling it once. GET, PUT, DELETE are naturally idempotent. For POST (which isn't), use idempotency keys to prevent duplicate operations.

Idempotency Key Pattern
POST /api/v1/payments Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890 // Server stores the key + result. If the same key is // received again, return the cached result โ€” never // process the payment twice. Stripe, Braintree, and // most payment APIs implement this pattern.
โš  Common Mistakes

Using verbs in URLs, returning 200 for errors, inconsistent naming conventions (camelCase vs snake_case), not versioning from day one, returning massive payloads without pagination, and ignoring HATEOAS constraints.


// 02 โ€” Backend Architecture

Backend Architecture

Clean Architecture separates your application into concentric layers with a clear dependency rule: inner layers never depend on outer layers.

Clean Architecture Overview

Robert C. Martin's Clean Architecture keeps your business logic independent of frameworks, databases, and external services. This makes your code testable, maintainable, and replaceable.

DEPENDENCY FLOW โ€” outer layers depend on inner layers

Infrastructure (DB, External Services, File System)
โ†• implements interfaces from
Application (Use Cases, Commands, Queries, DTOs)
โ†• depends on
Domain (Entities, Value Objects, Domain Events, Interfaces)

Project Folder Structure

๐Ÿ“ my-app/ โ”œโ”€โ”€ ๐Ÿ“ src/ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ controllers/ # HTTP endpoint handlers โ”‚ โ”œโ”€โ”€ ๐Ÿ“ middleware/ # Express middleware โ”‚ โ”œโ”€โ”€ ๐Ÿ“ routes/ # Route definitions โ”‚ โ”œโ”€โ”€ ๐Ÿ“ services/ # Business logic โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“ interfaces/ # Service contracts โ”‚ โ”œโ”€โ”€ ๐Ÿ“ repositories/ # Data access (TypeORM) โ”‚ โ”œโ”€โ”€ ๐Ÿ“ models/ # Domain entities โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ dto/ # Request / Response shapes โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“ schemas/ # Validation schemas (Zod) โ”‚ โ”œโ”€โ”€ ๐Ÿ“ config/ # App configuration โ”‚ โ””โ”€โ”€ ๐Ÿ“ utils/ # Shared helpers โ”œโ”€โ”€ ๐Ÿ“ tests/ # Jest / Vitest test suites โ”œโ”€โ”€ package.json โ”œโ”€โ”€ tsconfig.json โ””โ”€โ”€ .env โ”œโ”€โ”€ ๐Ÿ“ tests/ # Jest / Vitest test suites โ”œโ”€โ”€ package.json โ”œโ”€โ”€ tsconfig.json โ””โ”€โ”€ .env
๐Ÿ“ my-app/ โ”œโ”€โ”€ ๐Ÿ“ app/ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ api/ # Route handlers โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“ v1/ # Versioned endpoints โ”‚ โ”œโ”€โ”€ ๐Ÿ“ core/ # Config, security, dependencies โ”‚ โ”œโ”€โ”€ ๐Ÿ“ models/ # SQLAlchemy ORM models โ”‚ โ”œโ”€โ”€ ๐Ÿ“ schemas/ # Pydantic schemas (DTOs) โ”‚ โ”œโ”€โ”€ ๐Ÿ“ services/ # Business logic โ”‚ โ”œโ”€โ”€ ๐Ÿ“ repositories/ # Data access layer โ”‚ โ”œโ”€โ”€ ๐Ÿ“ middleware/ # Custom middleware โ”‚ โ””โ”€โ”€ ๐Ÿ“ utils/ # Shared helpers โ”œโ”€โ”€ ๐Ÿ“ alembic/ # Database migrations โ”œโ”€โ”€ ๐Ÿ“ tests/ # Pytest test suites โ”œโ”€โ”€ main.py # App entry point โ”œโ”€โ”€ requirements.txt โ”œโ”€โ”€ pyproject.toml โ””โ”€โ”€ .env
๐Ÿ“ my-app/ โ”œโ”€โ”€ ๐Ÿ“ app/ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Http/ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Controllers/ # HTTP endpoints โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Middleware/ # Request pipeline โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“ Requests/ # Form Request validation โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Models/ # Eloquent models โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Services/ # Business logic โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Repositories/ # Data access layer โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“ Interfaces/ # Repository contracts โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Providers/ # Service providers (DI) โ”‚ โ””โ”€โ”€ ๐Ÿ“ Events/ # Domain events โ”œโ”€โ”€ ๐Ÿ“ routes/ โ”‚ โ””โ”€โ”€ api.php # API route definitions โ”œโ”€โ”€ ๐Ÿ“ database/ โ”‚ โ””โ”€โ”€ ๐Ÿ“ migrations/ # Database migrations โ”œโ”€โ”€ ๐Ÿ“ tests/ # PHPUnit test suites โ”œโ”€โ”€ composer.json โ”œโ”€โ”€ .env โ””โ”€โ”€ artisan
๐Ÿ“ MyApp.sln โ”œโ”€โ”€ ๐Ÿ“ API/ # Entry point โ€” ASP.NET Core โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Controllers/ # HTTP endpoints โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Middleware/ # Request pipeline middleware โ”‚ โ”œโ”€โ”€ ๐Ÿ“ DTOs/ # Request / Response models โ”‚ โ””โ”€โ”€ ๐Ÿ“ Extensions/ # DI registration, Swagger config โ”œโ”€โ”€ ๐Ÿ“ Application/ # Business logic / use cases โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Services/ # Service implementations โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Interfaces/ # IUserService, IOrderService โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Commands/ # CQRS write operations โ”‚ โ””โ”€โ”€ ๐Ÿ“ Queries/ # CQRS read operations โ”œโ”€โ”€ ๐Ÿ“ Domain/ # Pure business rules โ”‚ โ”œโ”€โ”€ ๐Ÿ“ Entities/ # User, Order, Product โ”‚ โ”œโ”€โ”€ ๐Ÿ“ ValueObjects/ # Email, Money, Address โ”‚ โ””โ”€โ”€ ๐Ÿ“ DomainEvents/ # OrderPlaced, UserRegistered โ””โ”€โ”€ ๐Ÿ“ Infrastructure/ # External concerns โ”œโ”€โ”€ ๐Ÿ“ Repositories/ # EF Core implementations โ”œโ”€โ”€ ๐Ÿ“ Persistence/ # DbContext, migrations โ””โ”€โ”€ ๐Ÿ“ ExternalServices/ # Email, payment, storage

Dependency Injection

TypeScript โ€” container.ts (tsyringe)
import { container } from 'tsyringe'; import { UserService } from './services/UserService'; import { UserRepository } from './repositories/UserRepository'; import { CacheService } from './services/CacheService'; // Register dependencies container.registerSingleton('IUserService', UserService); container.registerSingleton('IUserRepository', UserRepository); container.registerSingleton('ICacheService', CacheService); container.register('IEmailService', { useClass: EmailService }); // Singleton โ€” one instance for app lifetime (caches, configs) // Transient โ€” new instance every resolution (default) // Scoped โ€” use request-scoped containers (per request)
Python โ€” core/deps.py (FastAPI Depends)
from fastapi import Depends from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_session from app.repositories.user_repository import UserRepository from app.services.user_service import UserService async def get_user_repository( session: AsyncSession = Depends(get_session) ) -> UserRepository: return UserRepository(session) async def get_user_service( repo: UserRepository = Depends(get_user_repository), cache: CacheService = Depends(CacheService) ) -> UserService: return UserService(repo, cache) # FastAPI resolves dependencies automatically per-request # Each Depends() creates a new instance per request (scoped) # Use lru_cache for singleton-like behavior
PHP โ€” AppServiceProvider.php (Laravel)
use App\Services\UserService; use App\Services\Interfaces\IUserService; use App\Repositories\UserRepository; use App\Repositories\Interfaces\IUserRepository; public function register(): void { // Bind interfaces to implementations $this->app->bind(IUserService::class, UserService::class); $this->app->bind(IUserRepository::class, UserRepository::class); $this->app->singleton(ICacheService::class, RedisCacheService::class); // bind โ€” new instance per resolution (transient) // scoped โ€” new instance per request lifecycle // singleton โ€” one instance for app lifetime }
C# โ€” Program.cs โ€” Service Registration
var builder = WebApplication.CreateBuilder(args); // Register services with appropriate lifetimes builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IUserRepository, UserRepository>(); builder.Services.AddSingleton<ICacheService, RedisCacheService>(); builder.Services.AddTransient<IEmailService, EmailService>(); // Scoped โ€” new instance per HTTP request (most common) // Singleton โ€” one instance for app lifetime (caches, configs) // Transient โ€” new instance every time it's requested

Repository Pattern

TypeScript โ€” Repository Interface + TypeORM Implementation
// src/repositories/interfaces/IUserRepository.ts export interface IUserRepository { findById(id: string): Promise<User | null>; findAll(filter: UserFilter): Promise<User[]>; create(user: User): Promise<User>; update(user: User): Promise<User>; delete(id: string): Promise<void>; } // src/repositories/UserRepository.ts import { EntityRepository, Repository } from "typeorm"; import { User } from "../models/User"; @EntityRepository(User) export class UserRepository extends Repository implements IUserRepository { async findById(id: string): Promise { return this.findOne(id); } // ...other methods }
Python โ€” Repository with SQLAlchemy
# app/repositories/user_repository.py from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.user import User import uuid class UserRepository: def __init__(self, session: AsyncSession): self.session = session async def get_by_id(self, user_id: uuid.UUID) -> User | None: result = await self.session.execute( select(User).where(User.id == user_id) ) return result.scalar_one_or_none() async def get_all(self, filter: UserFilter) -> list[User]: query = select(User) return (await self.session.execute(query)).scalars().all()
PHP โ€” Repository Interface + Eloquent Implementation
// app/Repositories/Interfaces/IUserRepository.php interface IUserRepository { public function findById(string $id): ?User; public function findAll(UserFilter $filter): Collection; public function create(array $data): User; public function update(User $user, array $data): User; public function delete(string $id): void; } // app/Repositories/UserRepository.php class UserRepository implements IUserRepository { public function __construct(private User $model) {} public function findById(string $id): ?User { return $this->model->find($id); } }
C# โ€” Repository Interface + Implementation
// Domain/Interfaces/IUserRepository.cs public interface IUserRepository { Task<User?> GetByIdAsync(Guid id); Task<IEnumerable<User>> GetAllAsync(UserFilter filter); Task AddAsync(User user); Task UpdateAsync(User user); Task DeleteAsync(Guid id); } // Infrastructure/Repositories/UserRepository.cs public class UserRepository : IUserRepository { private readonly AppDbContext _context; public UserRepository(AppDbContext context) => _context = context; public async Task<User?> GetByIdAsync(Guid id) => await _context.Users.FindAsync(id); }

Request Flow Through the Architecture

HTTP Request
โ†’
Middleware Pipeline
โ†’
Controller
โ†’
Service
โ†’
Repository
โ†’
Database

Response follows the reverse path, mapping Domain โ†’ DTO at the Service layer

Global Exception Handling Middleware

TypeScript โ€” Express Error Handler
// src/middleware/errorHandler.ts export const errorHandler = (err, req, res, next) => { if (err instanceof NotFoundError) { logger.warn(err.message); return res.status(404).json({ success: false, message: err.message }); } if (err instanceof ValidationError) { return res.status(422).json({ success: false, message: 'Validation failed', errors: err.details }); } logger.error('Unhandled exception', err); res.status(500).json({ success: false, message: 'An unexpected error occurred' }); };
Python โ€” FastAPI Exception Handlers
# app/middleware/error_handler.py from fastapi import Request from fastapi.responses import JSONResponse @app.exception_handler(NotFoundException) async def not_found_handler(req: Request, exc: NotFoundException): logger.warning(exc.message) return JSONResponse(status_code=404, content={"success": False, "message": exc.message}) @app.exception_handler(ValidationError) async def validation_handler(req: Request, exc: ValidationError): return JSONResponse(status_code=422, content={"success": False, "message": "Validation failed", "errors": exc.errors()}) @app.exception_handler(Exception) async def generic_handler(req: Request, exc: Exception): logger.error(f"Unhandled exception: {exc}") return JSONResponse(status_code=500, content={"success": False, "message": "An unexpected error occurred"})
PHP โ€” Laravel Exception Handler
// app/Exceptions/Handler.php class Handler extends ExceptionHandler { public function render($request, Throwable $e): JsonResponse { if ($e instanceof ModelNotFoundException) { return response()->json([ 'success' => false, 'message' => 'Resource not found' ], 404); } if ($e instanceof ValidationException) { return response()->json([ 'success' => false, 'message' => 'Validation failed', 'errors' => $e->errors() ], 422); } Log::error('Unhandled exception', ['exception' => $e]); return response()->json([ 'success' => false, 'message' => 'An unexpected error occurred' ], 500); } }
C# โ€” Exception Middleware
public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger) { public async Task InvokeAsync(HttpContext ctx) { try { await next(ctx); } catch (NotFoundException ex) { logger.LogWarning(ex.Message); await WriteErrorAsync(ctx, 404, ex.Message); } catch (ValidationException ex) { await WriteErrorAsync(ctx, 422, "Validation failed", ex.Errors); } catch (Exception ex) { logger.LogError(ex, "Unhandled exception"); await WriteErrorAsync(ctx, 500, "An unexpected error occurred"); } } }

Validation & Mapping

TypeScript โ€” Zod Validation + Mapping
// Zod โ€” TypeScript-first validation import { z } from 'zod'; const CreateUserSchema = z.object({ email: z.string().email(), password: z.string().min(8).regex(/[A-Z]/), name: z.string().min(1).max(100), }); type CreateUserDto = z.infer<typeof CreateUserSchema>; // Mapping โ€” simple spread or class-transformer const toUserResponse = (user: User): UserResponseDto => ({ id: user.id, email: user.email, name: user.name, createdAt: user.createdAt, });
Python โ€” Pydantic Validation + Mapping
# Pydantic โ€” automatic validation via type annotations from pydantic import BaseModel, EmailStr, Field, field_validator import re class CreateUserDto(BaseModel): email: EmailStr password: str = Field(min_length=8) name: str = Field(min_length=1, max_length=100) @field_validator('password') def password_must_have_uppercase(cls, v): if not re.search(r'[A-Z]', v): raise ValueError('Must contain uppercase letter') return v # Mapping โ€” Pydantic handles serialization automatically class UserResponseDto(BaseModel): id: uuid.UUID email: str name: str class Config: from_attributes = True # auto-maps from ORM objects
PHP โ€” Laravel Form Request + API Resources
// Form Request โ€” self-validating request objects class CreateUserRequest extends FormRequest { public function rules(): array { return [ 'email' => 'required|email|unique:users', 'password' => 'required|min:8|regex:/[A-Z]/', 'name' => 'required|max:100', ]; } } // API Resources โ€” response mapping / transformation class UserResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'email' => $this->email, 'name' => $this->name, 'createdAt' => $this->created_at->toISOString(), ]; } }
C# โ€” FluentValidation + AutoMapper
// FluentValidation โ€” expressive, testable validation rules public class CreateUserValidator : AbstractValidator<CreateUserDto> { public CreateUserValidator() { RuleFor(x => x.Email).NotEmpty().EmailAddress(); RuleFor(x => x.Password).MinimumLength(8).Matches(@"[A-Z]"); RuleFor(x => x.Name).NotEmpty().MaximumLength(100); } } // AutoMapper โ€” eliminates manual mapping boilerplate public class UserProfile : Profile { public UserProfile() { CreateMap<User, UserResponseDto>(); CreateMap<CreateUserDto, User>() .ForMember(d => d.Id, o => o.MapFrom(_ => Guid.NewGuid())); } }

// 03 โ€” Authentication & Security

API Authentication and Security

Security is not a feature you add at the end. It must be designed into every layer of your API from day one.

JWT Authentication

JSON Web Tokens are self-contained, signed tokens that carry user claims. A JWT has three base64url-encoded parts separated by dots: Header.Payload.Signature.

JWT Structure
// Header โ€” algorithm + token type { "alg": "HS256", "typ": "JWT" } // Payload โ€” claims (never put sensitive data here!) { "sub": "user-uuid-here", "email": "user@example.com", "roles": ["admin", "editor"], "iat": 1700000000, // issued at "exp": 1700003600 // expires at (1 hour) } // Signature โ€” HMAC-SHA256(base64(header)+"."+base64(payload), secret) // Signature ensures the token wasn't tampered with

OAuth2 vs JWT โ€” Key Differences

Aspect JWT OAuth2
What it is Token format / encoding standard Authorization framework / protocol
Purpose Represent claims securely Delegate access to resources
Used for API authentication between services Third-party app access (e.g. "Login with Google")
Statefulness Stateless (server doesn't store) Stateful (authorization server manages tokens)

Authorization Strategies

TypeScript โ€” Role + Permission Middleware
// Role-based โ€” simple, coarse-grained const requireRole = (...roles) => (req, res, next) => { if (!roles.includes(req.user.role)) { return res.status(403).json({ message: 'Forbidden' }); } next(); }; // Permission-based โ€” finer control const requirePermission = (permission) => (req, res, next) => { if (!req.user.permissions?.includes(permission)) { return res.status(403).json({ message: 'Forbidden' }); } next(); }; // Usage in routes router.delete('/users/:id', requireRole('admin', 'moderator'), deleteUser); router.post('/posts/publish', requirePermission('content:publish'), publishPost);
Python โ€” FastAPI Role + Permission Dependencies
# Role-based โ€” dependency injection pattern def require_role(*roles): def checker(user = Depends(get_current_user)): if user.role not in roles: raise HTTPException(status_code=403, detail="Forbidden") return user return checker # Permission-based โ€” finer control def require_permission(permission: str): def checker(user = Depends(get_current_user)): if permission not in user.permissions: raise HTTPException(status_code=403, detail="Forbidden") return user return checker # Usage in route handlers @router.delete("/users/{user_id}") async def delete_user(user_id: UUID, user = Depends(require_role("admin", "moderator"))): ... @router.post("/posts/publish") async def publish_post(post_id: UUID, user = Depends(require_permission("content:publish"))): ...
PHP โ€” Laravel Middleware + Policies
// Role-based โ€” middleware Route::delete('/users/{id}', [UserController::class, 'destroy']) ->middleware('role:admin,moderator'); // Policy-based โ€” fine-grained control // app/Policies/PostPolicy.php class PostPolicy { public function publish(User $user): bool { return $user->hasPermission('content:publish'); } } // In controller public function publish(Post $post) { $this->authorize('publish', $post); } // Gate definition in AuthServiceProvider Gate::define('publish-content', function (User $user) { return $user->permissions->contains('content:publish'); });
C# โ€” Role + Policy Based Authorization
// Role-based โ€” simple, coarse-grained [Authorize(Roles = "Admin,Moderator")] public IActionResult DeleteUser(Guid id) { } // Claims-based โ€” finer control [Authorize(Policy = "CanPublishContent")] public IActionResult PublishPost(Guid postId) { } // Policy definition in Program.cs builder.Services.AddAuthorization(opts => { opts.AddPolicy("CanPublishContent", policy => policy.RequireClaim("permission", "content:publish")); });

Refresh Token Strategy

Best Practice

Access tokens should expire in 15โ€“60 minutes. Store long-lived refresh tokens (7โ€“30 days) securely in an HTTP-only cookie. On expiry, exchange the refresh token for a new access token without re-authenticating the user. Rotate refresh tokens on each use and revoke them on logout.

OWASP Top Security Concerns

โœ—
SQL Injection โ€” Use parameterized queries or ORMs. Never concatenate user input into SQL strings.
โœ—
Broken Authentication โ€” Weak passwords, no MFA, tokens not rotating, credentials in URLs.
โœ—
Excessive Data Exposure โ€” Returning entire database objects. Always use DTOs to control response shape.
โœ—
Missing Rate Limiting โ€” Allows brute force attacks and credential stuffing. Implement per-IP and per-user throttling.
โœ—
Insecure Direct Object References โ€” Exposing numeric IDs that can be enumerated. Use UUIDs and verify ownership.

API Security Checklist

โœ“
HTTPS enforced everywhere โ€” redirect all HTTP to HTTPS, HSTS enabled
โœ“
Rate limiting applied โ€” token bucket or sliding window per endpoint
โœ“
CORS configured โ€” explicit allowed origins, no wildcard in production
โœ“
Input validated โ€” all request bodies validated with schema/FluentValidation
โœ“
Secrets in environment variables โ€” never hardcoded in source code
โœ“
Structured logging and monitoring โ€” audit trail for sensitive operations
โœ“
Dependency scanning โ€” automated CVE checks in CI/CD pipeline
โœ“
Token expiry enforced โ€” short-lived access tokens with refresh rotation

// 04 โ€” Distributed Systems

API Gateway and Microservices

Microservices decompose a system into independently deployable services. The API Gateway is the single entry point that manages cross-cutting concerns so your services don't have to.

Monolith vs Microservices

Aspect Monolith Microservices
Deployment Deploy entire application Deploy individual services
Scaling Scale everything together Scale hotspots independently
Tech stack Homogeneous Polyglot (different tech per service)
Failure isolation One bug can crash everything Failure is contained to a service
Complexity Simple to develop initially High operational complexity
Best for Small teams, early-stage products Large teams, mature domains
โš  Architect's Advice

Start with a monolith. Extract microservices only when you have clear domain boundaries and team scaling problems. "Premature microservices" is one of the most expensive architectural mistakes.

API Gateway Architecture

Mobile Client ยท Web App ยท Third-Party
โ†“ HTTPS
API Gateway
Auth ยท Rate Limiting ยท Routing ยท Logging ยท SSL Termination
โ†“ routes to
User Service
Order Service
Payment Service
Notification Svc
โ†“ each connects to its own
Database (per service โ€” Users DB, Orders DB, Payments DB)

Service-to-Service Communication

๐Ÿ”„

Synchronous (HTTP/gRPC)

Direct HTTP calls or high-performance gRPC. Simple but creates tight coupling. Use for real-time data needs.

๐Ÿ“จ

Asynchronous (Message Bus)

Services publish events to a broker (RabbitMQ, Kafka). Consumers react independently. Loose coupling, high resilience.

๐Ÿ”

Service Discovery

Services register themselves (Consul, Kubernetes DNS, Azure Service Fabric). Gateway resolves names to IPs dynamically.

Gateway Tools Comparison

Nginx Kong Azure API Management AWS API Gateway
Express Gatewayhttp-proxy-middleware
FastAPI GatewayTraefik
Laravel OctaneTraefik
YARP (C#)Ocelot (C#)
Tool Best For Key Features
Nginx Reverse proxy, load balancing High performance, SSL termination, static serving
Kong API management at scale Plugin ecosystem, rate limiting, auth plugins
Azure API Management Enterprise / Azure ecosystems Policy engine, developer portal, analytics
AWS API Gateway Serverless / Lambda backends Auto-scaling, IAM auth, throttling
Express Gateway Node.js API gateway Middleware-based, declarative config, plugin system
YARP .NET reverse proxy in-process Programmatic routing, middleware integration

// 05 โ€” High-Performance Systems

Performance Optimization

Performance is a feature. Slow APIs destroy user experience and inflate infrastructure costs. Optimize at every layer โ€” code, database, network, and infrastructure.

Caching Strategies

โšก

In-Memory Cache

In-process caching (node-cache, lru-cache, cachetools, IMemoryCache). Fast, zero network overhead. Limited to single process โ€” data is lost on restart and not shared across instances.

๐Ÿ—ƒ๏ธ

Redis Distributed Cache

Shared across all API instances. Survives restarts. Supports rich data structures, pub/sub, and expiry policies. Industry standard for distributed caching.

๐Ÿ“ก

Response Caching

HTTP-level caching via Cache-Control headers. CDNs (Cloudflare, Azure CDN) cache responses at the edge, closest to the user.

TypeScript โ€” Redis Caching Pattern
async getUserById(userId: string): Promise<UserDto> { const cacheKey = `user:${userId}`; // 1. Try cache first const cached = await redis.get(cacheKey); if (cached) return JSON.parse(cached); // 2. Cache miss โ€” query database const user = await userRepository.findOne({ where: { id: userId } }); if (!user) throw new NotFoundError(`User ${userId} not found`); // 3. Store in cache with TTL (15 minutes) await redis.setex(cacheKey, 900, JSON.stringify(user)); return toUserDto(user); }
Python โ€” Redis Caching Pattern
async def get_user(self, user_id: UUID) -> UserDto: cache_key = f"user:{user_id}" # 1. Try cache first cached = await self.redis.get(cache_key) if cached: return UserDto.model_validate_json(cached) # 2. Cache miss โ€” query database user = await self.repo.get_by_id(user_id) if not user: raise NotFoundException(f"User {user_id} not found") # 3. Store in cache with TTL (15 minutes) dto = UserDto.from_orm(user) await self.redis.setex(cache_key, 900, dto.model_dump_json()) return dto
PHP โ€” Laravel Cache Pattern
public function getUser(string $userId): UserDto { $cacheKey = "user:{$userId}"; // Cache::remember handles try-cache + fallback-to-db return Cache::remember($cacheKey, now()->addMinutes(15), function () use ($userId) { $user = $this->repository->findById($userId); if (!$user) { throw new ModelNotFoundException("User not found"); } // Automatically cached for 15 minutes return UserResource::make($user); } ); }
C# โ€” Redis Caching Pattern
public async Task<UserDto> GetUserAsync(Guid userId) { var cacheKey = $"user:{userId}"; // 1. Try cache first var cached = await _redis.GetStringAsync(cacheKey); if (cached is not null) return JsonSerializer.Deserialize<UserDto>(cached)!; // 2. Cache miss โ€” query database var user = await _repo.GetByIdAsync(userId) ?? throw new NotFoundException($"User {userId} not found"); // 3. Store in cache with TTL await _redis.SetStringAsync(cacheKey, JsonSerializer.Serialize(user), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15) }); return _mapper.Map<UserDto>(user); }

Database Optimization

TypeScript โ€” TypeORM Eager Loading
// โŒ BAD โ€” N+1: separate query for each user const orders = await orderRepository.find(); for (const order of orders) { const user = await userRepository.findOne(order.userId); // N queries! } // โœ… GOOD โ€” eager loading with relations const orders = await orderRepository.find({ relations: ["user", "items", "items.product"], });
Python โ€” SQLAlchemy Eager Loading
# โŒ BAD โ€” N+1: separate query for each user orders = await session.execute(select(Order)) for order in orders.scalars(): user = await session.get(User, order.user_id) # N queries! # โœ… GOOD โ€” eager loading with joinedload from sqlalchemy.orm import joinedload orders = await session.execute( select(Order) .options(joinedload(Order.user)) .options(joinedload(Order.items).joinedload(Item.product)) )
PHP โ€” Eloquent Eager Loading
// โŒ BAD โ€” N+1: separate query for each user $orders = Order::all(); foreach ($orders as $order) { $user = User::find($order->user_id); // N queries! } // โœ… GOOD โ€” eager loading with with() $orders = Order::with(['user', 'items.product'])->get(); // Or prevent N+1 globally in AppServiceProvider Model::preventLazyLoading(!app()->isProduction());
EF Core โ€” Avoiding N+1 with Eager Loading
// โŒ BAD โ€” N+1 problem: 1 query for orders + N queries for each user var orders = await _context.Orders.ToListAsync(); foreach (var order in orders) { var user = await _context.Users.FindAsync(order.UserId); // N queries! } // โœ… GOOD โ€” eager loading: 1 query with JOIN var orders = await _context.Orders .Include(o => o.User) .Include(o => o.Items).ThenInclude(i => i.Product) .ToListAsync();

Performance Bottlenecks to Watch

๐Ÿข

Synchronous Blocking

Thread starvation under load. Every database call, file I/O, or HTTP call must be async.

๐Ÿ—ƒ๏ธ

Missing Indexes

Full table scans on million-row tables. Profile and add indexes on hot query paths.

๐Ÿ”

Chat-ty APIs

Multiple small requests instead of one well-designed batch request. Increases latency and network overhead.

๐Ÿ’พ

Over-fetching Data

Returning 50 fields when the client needs 5. Use projections, DTOs, and sparse fieldsets.


// 06 โ€” Developer Experience

API Documentation and Developer Experience

A great API with poor documentation will fail. Developer experience (DX) is as important as technical correctness. If developers can't understand your API in 5 minutes, they'll find another one.

Swagger / OpenAPI

OpenAPI Specification (OAS) is the industry standard for API documentation. It's machine-readable (YAML/JSON) and generates interactive docs automatically.

TypeScript โ€” swagger-jsdoc + Express
/** * @openapi * /api/users: * post: * summary: Create a new user account * tags: [Users] * requestBody: * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/CreateUserDto' * responses: * 201: * description: User created successfully * 400: * description: Validation failed * 409: * description: Email already exists */ router.post('/users', validate(CreateUserSchema), async (req, res) => { const result = await userService.create(req.body); res.status(201).json(result); });
Python โ€” FastAPI (automatic OpenAPI built-in)
from fastapi import APIRouter, status router = APIRouter(prefix="/api/users", tags=["Users"]) @router.post( "/", response_model=UserResponseDto, status_code=status.HTTP_201_CREATED, summary="Create a new user account", responses={ 400: {"description": "Validation failed"}, 409: {"description": "Email already exists"}, } ) async def create_user( request: CreateUserDto, service: UserService = Depends(get_user_service) ): """Create a new user account with email and password.""" return await service.create(request)
PHP โ€” Laravel Scribe Documentation
/** * Create a new user account * * @group Users * @bodyParam email string required Example: user@example.com * @bodyParam password string required Min 8 chars. Example: SecurePass1 * @bodyParam name string required Example: John Doe * @response 201 {"id": "uuid", "email": "user@example.com"} * @response 400 {"message": "Validation failed"} * @response 409 {"message": "Email already exists"} */ public function store(CreateUserRequest $request): JsonResponse { $user = $this->userService->create($request->validated()); return (new UserResource($user)) ->response()->setStatusCode(201); }
C# โ€” XML Documentation for Swagger
/// <summary>Create a new user account</summary> /// <param name="request">User registration details</param> /// <response code="201">User created successfully</response> /// <response code="400">Validation failed</response> /// <response code="409">Email already exists</response> [HttpPost] [ProducesResponseType(typeof(UserResponseDto), 201)] [ProducesResponseType(typeof(ErrorResponse), 400)] public async Task<IActionResult> CreateUser([FromBody] CreateUserDto request) { var result = await _userService.CreateAsync(request); return CreatedAtAction(nameof(GetUser), new { id = result.Id }, result); }

Standardized Error Response Format

Consistent error responses make life dramatically easier for API consumers. Always use the same error shape across your entire API.

JSON โ€” Standard Error Response
{ "success": false, "message": "Validation failed", "statusCode": 422, "errors": [ "Email is required", "Password must be at least 8 characters" ], "traceId": "00-abc123def456-789ghi-01" // for log correlation } // Success response โ€” equally consistent { "success": true, "data": { "id": "uuid", "email": "user@example.com" }, "meta": { "page": 1, "total": 248, "pageSize": 25 } }

Documentation Checklist

โœ“
Every endpoint documented with description, parameters, and example responses
โœ“
Authentication clearly explained โ€” how to get a token, how to use it
โœ“
Error codes documented โ€” what each 4xx/5xx means in your API's context
โœ“
Rate limits clearly communicated โ€” limits, headers, retry-after
โœ“
Changelog maintained โ€” breaking changes clearly communicated
โœ“
Postman / Insomnia collection exported and shared
โœ“
Getting started guide โ€” working example in under 5 minutes

// 07 โ€” Data Architecture

Database Design for APIs

Database decisions made early are the hardest to undo. Choose your storage engine based on your data's shape, consistency requirements, and query patterns โ€” not trends.

SQL vs NoSQL Decision Framework

Factor Choose SQL Choose NoSQL
Data relationships Complex, many foreign keys Simple, document-based
Schema Well-defined, stable schema Flexible, evolving schema
Transactions ACID required (financial, orders) Eventual consistency acceptable
Scale pattern Vertical scaling, read replicas Horizontal sharding
Query complexity Complex joins, aggregations Key lookups, simple filters
Use cases Banking, e-commerce, ERP Catalogs, analytics, logs, IoT

Practical Examples

๐Ÿฆ

Banking System โ†’ SQL

Accounts, transactions, and transfers require ACID guarantees. A transfer must debit one account AND credit another atomically โ€” partial failure is catastrophic.

๐Ÿ“Š

Analytics Platform โ†’ NoSQL

Millions of events per second with flexible schemas. Time-series or document stores (MongoDB, InfluxDB) handle write-heavy, schema-free workloads better.

๐Ÿ›’

E-commerce โ†’ Hybrid

Product catalog in MongoDB (flexible attributes), orders in PostgreSQL (ACID), and session cart in Redis (fast, ephemeral).

Transaction Handling & Microservice Boundaries

โš  Distributed Transactions

In microservices, each service owns its database. You cannot use SQL transactions across service boundaries. Use the Saga pattern โ€” a sequence of local transactions, each publishing an event to trigger the next step. On failure, compensating transactions undo previous steps.

TypeScript โ€” TypeORM Transaction
// TypeORM Transaction import { getManager } from "typeorm"; const result = await getManager().transaction(async transactionalEntityManager => { const order = await transactionalEntityManager.save(Order, { userId, total }); await transactionalEntityManager.decrement(Inventory, { id: inventoryId }, "quantity", orderQuantity); // deduct stock return order; // both succeed or neither does }); // If any operation throws, the entire transaction rolls back
Python โ€” SQLAlchemy Transaction
# SQLAlchemy async session with context manager async with session.begin(): order = Order(user_id=user_id, total=total) session.add(order) inventory.quantity -= order_quantity # deduct stock session.add(inventory) # both succeed or neither does # session.begin() auto-commits on exit, rolls back on exception
PHP โ€” Laravel DB Transaction
// Laravel's DB::transaction handles commit/rollback $order = DB::transaction(function () use ($userId, $total, $orderQuantity) { $order = Order::create([ 'user_id' => $userId, 'total' => $total, ]); $inventory = Inventory::lockForUpdate()->find($inventoryId); $inventory->decrement('quantity', $orderQuantity); // deduct stock return $order; // both succeed or neither does }); // If any exception is thrown, the entire transaction rolls back
C# โ€” Transaction Handling in EF Core
await using var transaction = await _context.Database.BeginTransactionAsync(); try { var order = new Order { UserId = userId, Total = total }; await _context.Orders.AddAsync(order); inventory.Quantity -= orderQuantity; // deduct stock _context.Inventory.Update(inventory); await _context.SaveChangesAsync(); await transaction.CommitAsync(); // both succeed or neither does } catch { await transaction.RollbackAsync(); throw; }

// 08 โ€” Architecture Patterns

API Design Patterns

Patterns are proven solutions to recurring problems. Knowing when to apply each pattern โ€” and when not to โ€” separates senior engineers from architects.

๐ŸŒ

REST

Resource-based, HTTP-native. Best for public APIs, CRUD operations, and browser clients. Mature tooling and universal understanding.

๐Ÿ”บ

GraphQL

Client specifies exact data shape. Eliminates over/under-fetching. Great for complex UIs and mobile. Higher complexity server-side.

โšก

gRPC

Binary protocol, strongly typed contracts. Extremely fast for service-to-service. Not browser-friendly without a proxy layer.

๐ŸŽฃ

Webhooks

Server pushes events to your endpoint. Ideal for async notifications (payment succeeded, order shipped). Requires retry logic and signature verification.

๐Ÿ“‹

CQRS

Separate read (Query) and write (Command) models. Reads can be optimized independently from writes. Often paired with Event Sourcing.

๐ŸŽฏ

Event-Driven

Services communicate via events on a message broker. Highest decoupling. Complex to debug. Best for async workflows and audit trails.

CQRS in Practice

TypeScript โ€” CQRS Command/Query Pattern
// Command โ€” write operation with side effects class CreateOrderCommand { constructor( public userId: string, public items: OrderItem[] ) {} } class CreateOrderHandler { async handle(cmd: CreateOrderCommand): Promise<string> { const order = Order.create(cmd.userId, cmd.items); await this.orderRepo.save(order); await this.eventBus.publish(new OrderCreatedEvent(order.id)); return order.id; } } // Query โ€” pure read, no side effects, can be cached class GetOrderQuery { constructor(public orderId: string) {} } // Controller dispatches to handler โ€” stays thin const orderId = await mediator.send(new CreateOrderCommand(userId, items));
Python โ€” CQRS Dataclass Pattern
from dataclasses import dataclass # Command โ€” write operation with side effects @dataclass class CreateOrderCommand: user_id: UUID items: list[OrderItem] class CreateOrderHandler: async def handle(self, cmd: CreateOrderCommand) -> UUID: order = Order.create(cmd.user_id, cmd.items) await self.repo.save(order) await self.event_bus.publish(OrderCreatedEvent(order.id)) return order.id # Query โ€” pure read, no side effects, can be cached @dataclass class GetOrderQuery: order_id: UUID # Route dispatches to handler โ€” stays thin order_id = await mediator.send(CreateOrderCommand(user_id, items))
PHP โ€” Laravel CQRS Pattern
// Command โ€” write operation with side effects class CreateOrderCommand { public function __construct( public readonly string $userId, public readonly array $items ) {} } class CreateOrderHandler { public function handle(CreateOrderCommand $cmd): string { $order = Order::create($cmd->userId, $cmd->items); $this->repo->save($order); event(new OrderCreatedEvent($order->id)); return $order->id; } } // Query โ€” pure read, no side effects, can be cached class GetOrderQuery { public function __construct(public readonly string $orderId) {} } // Controller dispatches to handler โ€” stays thin $orderId = app(Mediator::class)->send(new CreateOrderCommand($userId, $items));
C# โ€” MediatR CQRS Pattern
// Command โ€” write operation with side effects public record CreateOrderCommand(Guid UserId, List<OrderItem> Items) : IRequest<Guid>; public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid> { public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct) { var order = Order.Create(cmd.UserId, cmd.Items); await _repo.AddAsync(order, ct); await _bus.PublishAsync(new OrderCreatedEvent(order.Id)); return order.Id; } } // Query โ€” pure read, no side effects, can be cached public record GetOrderQuery(Guid OrderId) : IRequest<OrderDto>; // Controller dispatches to MediatR โ€” stays thin var result = await _mediator.Send(new CreateOrderCommand(userId, items));

API-First Development

Design the API contract (OpenAPI spec) before writing a single line of implementation. Teams agree on the contract, then build in parallel โ€” frontend mocks the API while backend implements it. Reduces integration surprises and rework.


// 09 โ€” Cloud & DevOps

Cloud and DevOps for APIs

Modern APIs live in containers, ship through automated pipelines, and run on cloud infrastructure. DevOps is not optional for production-grade systems.

Docker Containerization

Dockerfile โ€” Node.js Optimized
# Multi-stage build โ€” smaller final image FROM node:20-alpine AS build WORKDIR /app COPY package*.json . RUN npm ci COPY . . RUN npm run build FROM node:20-alpine AS runtime WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules COPY package*.json . EXPOSE 8080 # Run as non-root for security USER node CMD ["node", "dist/server.js"]
Dockerfile โ€” Python / FastAPI Optimized
# Multi-stage build โ€” smaller final image FROM python:3.12-slim AS build WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt FROM python:3.12-slim AS runtime WORKDIR /app COPY --from=build /usr/local/lib/python3.12 /usr/local/lib/python3.12 COPY . . EXPOSE 8080 # Run as non-root for security RUN useradd -m appuser USER appuser CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
Dockerfile โ€” PHP / Laravel Optimized
# Multi-stage build โ€” smaller final image FROM composer:2 AS build WORKDIR /app COPY composer.json composer.lock . RUN composer install --no-dev --optimize-autoloader COPY . . FROM php:8.3-fpm-alpine AS runtime WORKDIR /app COPY --from=build /app . RUN docker-php-ext-install pdo pdo_mysql EXPOSE 9000 # Run as non-root for security USER www-data CMD ["php-fpm"]
Dockerfile โ€” ASP.NET Core Optimized
# Multi-stage build โ€” smaller final image FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app COPY *.csproj . RUN dotnet restore COPY . . RUN dotnet publish -c Release -o out FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app COPY --from=build /app/out . EXPOSE 8080 # Run as non-root for security USER app ENTRYPOINT ["dotnet", "MyApi.dll"]

CI/CD Pipeline

GitHub Actions โ€” Node.js API Pipeline
name: Build and Deploy on: push: branches: [main] jobs: build-test-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: { node-version: '20' } - name: Install & Build run: npm ci && npm run build - name: Run Tests run: npm test -- --coverage - name: Build Docker Image run: docker build -t myapi:${{ github.sha }} . - name: Push to Registry & Deploy run: | docker push myregistry/myapi:${{ github.sha }} kubectl set image deploy/myapi myapi=myapi:${{ github.sha }}
GitHub Actions โ€” Python API Pipeline
name: Build and Deploy on: push: branches: [main] jobs: build-test-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: { python-version: '3.12' } - name: Install Dependencies run: pip install -r requirements.txt - name: Run Tests run: pytest --cov=app tests/ - name: Build Docker Image run: docker build -t myapi:${{ github.sha }} . - name: Push to Registry & Deploy run: | docker push myregistry/myapi:${{ github.sha }} kubectl set image deploy/myapi myapi=myapi:${{ github.sha }}
GitHub Actions โ€” PHP / Laravel Pipeline
name: Build and Deploy on: push: branches: [main] jobs: build-test-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: { php-version: '8.3' } - name: Install Dependencies run: composer install --no-dev --optimize-autoloader - name: Run Tests run: php artisan test --coverage - name: Build Docker Image run: docker build -t myapi:${{ github.sha }} . - name: Push to Registry & Deploy run: | docker push myregistry/myapi:${{ github.sha }} kubectl set image deploy/myapi myapi=myapi:${{ github.sha }}
GitHub Actions โ€” .NET API Pipeline
name: Build and Deploy on: push: branches: [main] jobs: build-test-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: { dotnet-version: '8.0' } - name: Restore & Build run: dotnet build --configuration Release - name: Run Tests run: dotnet test --no-build --collect "XPlat Code Coverage" - name: Build Docker Image run: docker build -t myapi:${{ github.sha }} . - name: Push to Registry & Deploy run: | docker push myregistry/myapi:${{ github.sha }} az containerapp update --image myapi:${{ github.sha }}

Logging & Monitoring Stack

๐Ÿ“‹

Structured Logging

Use a structured logging library (Winston, Pino, structlog, Monolog, Serilog). Emit JSON logs with correlation IDs. Never use console.log / print in production.

๐Ÿ“Š

Application Insights

Azure's APM tool. Automatic request tracing, dependency tracking, live metrics, smart alerting, and distributed tracing across services.

๐Ÿ””

Health Checks

Expose health check endpoints (/health, /ready) for infrastructure monitoring. Kubernetes uses these for liveness and readiness probes.

Environment Configuration

โœ“ Best Practice โ€” 12-Factor App Config

Store configuration in environment variables, not in config files committed to source control. Use Azure Key Vault, AWS Secrets Manager, or HashiCorp Vault for secrets. Config files (.env, appsettings.json, config.yaml) for non-sensitive defaults only.


// 10 โ€” AI Integration

AI-Powered APIs

AI capabilities are now first-class features in backend systems. Integrating LLMs, embeddings, and semantic search opens entirely new product possibilities.

Common AI Endpoint Patterns

REST โ€” AI Endpoint Design
POST /api/ai/chat โ†’ conversational chatbot POST /api/ai/summarize โ†’ document summarization POST /api/ai/sentiment โ†’ sentiment analysis POST /api/ai/recommendation โ†’ personalized recommendations POST /api/ai/search โ†’ semantic/vector search POST /api/ai/embeddings โ†’ generate text embeddings GET /api/ai/models โ†’ list available models

OpenAI API Integration

TypeScript โ€” OpenAI Chat Completion
import OpenAI from 'openai'; async function chatAsync(userMessage: string, systemPrompt: string) { const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessage }, ], }); return response.choices[0].message.content; } // Route โ€” stream response for better UX router.post('/chat', async (req, res) => { const stream = await openai.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: req.body.message }], stream: true, }); res.setHeader('Content-Type', 'text/event-stream'); for await (const chunk of stream) { res.write(`data: ${chunk.choices[0]?.delta?.content ?? ''}\n\n`); } res.end(); });
Python โ€” OpenAI Chat Completion
from openai import AsyncOpenAI from fastapi.responses import StreamingResponse async def chat_async(user_message: str, system_prompt: str) -> str: client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) response = await client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ] ) return response.choices[0].message.content # Route โ€” stream response for better UX @router.post("/chat") async def chat(req: ChatRequest): async def generate(): stream = await client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": req.message}], stream=True, ) async for chunk in stream: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content return StreamingResponse(generate(), media_type="text/event-stream")
PHP โ€” OpenAI Chat Completion
use OpenAI; public function chatAsync(string $userMessage, string $systemPrompt): string { $client = OpenAI::client(config('services.openai.key')); $response = $client->chat()->create([ 'model' => 'gpt-4o', 'messages' => [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userMessage], ], ]); return $response->choices[0]->message->content; } // Route โ€” stream response for better UX Route::post('/chat', function (ChatRequest $req) { return response()->stream(function () use ($req) { $stream = OpenAI::chat()->createStreamed([ 'model' => 'gpt-4o', 'messages' => [['role' => 'user', 'content' => $req->message]], ]); foreach ($stream as $chunk) { echo "data: " . ($chunk->choices[0]->delta->content ?? '') . "\n\n"; ob_flush(); flush(); } }, 200, ['Content-Type' => 'text/event-stream']); });
C# โ€” OpenAI Chat Completion
public async Task<string> ChatAsync(string userMessage, string systemPrompt) { var client = new OpenAIClient(apiKey); var chat = client.GetChatClient("gpt-4o"); var response = await chat.CompleteChatAsync( [ ChatMessage.CreateSystemMessage(systemPrompt), ChatMessage.CreateUserMessage(userMessage) ]); return response.Value.Content[0].Text; } // Controller endpoint โ€” stream response for better UX [HttpPost("chat")] public async IAsyncEnumerable<string> Chat([FromBody] ChatRequest req) { await foreach (var chunk in _aiService.StreamChatAsync(req.Message)) yield return chunk; // Server-Sent Events via IAsyncEnumerable }

Semantic Search with Embeddings

Embeddings convert text into high-dimensional vectors that encode semantic meaning. Similar texts produce similar vectors. This enables searching by meaning, not just keywords.

User Query: "How do I cancel my subscription?"
โ†“ embed with OpenAI text-embedding-3-small
[0.23, -0.45, 0.81, ... 1536 dimensions]
โ†“ vector similarity search
pgvector / Pinecone / Azure AI Search
โ†“ nearest neighbors
Top 5 most semantically relevant documents returned

AI Integration Best Practices


// 11 โ€” Architect Mindset

System Design Thinking

The shift from developer to architect is not about knowing more technologies. It's about asking different questions at a different scale.

Developer Thinks About Architect Thinks About
How do I implement this endpoint? Should this be an endpoint, event, or job?
Does this code compile and pass tests? What happens when this service fails at 3am?
How do I store this data? How does this data model affect future team velocity?
How do I add this feature? How does this feature affect reliability and cost at 10x scale?
Performance of this query Latency profile of this entire user journey
This module's dependencies Organizational boundaries between teams

Designing for Millions of Users

โš–๏ธ

Horizontal Scaling

Add more instances behind a load balancer rather than bigger servers. Stateless API design is a prerequisite โ€” no local session state.

๐ŸŒ

CDN + Edge Caching

Serve static assets and cacheable API responses from edge locations nearest to the user. Reduces latency from 200ms to 10ms.

๐Ÿ“ฌ

Async Processing

Move heavy operations (emails, reports, image processing) off the request path into background jobs. Return 202 Accepted immediately.

๐Ÿ›ก๏ธ

Circuit Breakers

When downstream services are failing, stop hammering them. Open the circuit, return cached data or a graceful error, close it when healthy again.

๐Ÿ“‰

Database Sharding

Distribute data across multiple database instances. Partition by user ID, tenant, region, or date range.

๐Ÿ“Š

Observability

You can't optimize what you can't measure. Distributed tracing, metrics dashboards, and SLO monitoring are non-negotiable at scale.

The Architect's Principle

Every architectural decision is a trade-off. There are no silver bullets. The best architects clearly articulate why a decision was made, what was traded away, and under what conditions it should be revisited. Document your Architecture Decision Records (ADRs).


// 12 โ€” Common Questions

Developer FAQ

25 questions answered clearly for developers at every stage of their backend journey.

What is the difference between REST and GraphQL?+

REST uses fixed endpoints that return predefined data shapes. GraphQL uses a single endpoint where the client specifies exactly what data it needs. REST is simpler, better understood, and easier to cache. GraphQL shines when clients have very different data needs (mobile vs desktop), when over-fetching is a problem, or when you're aggregating multiple data sources. Use REST for public APIs and simple CRUD, GraphQL for complex UIs with variable data requirements.

What makes a good API design?+

A good API is predictable (consistent naming, consistent error shapes), self-documenting (clear resource names, standard HTTP methods), secure by default (auth required where needed, minimal data exposure), and versioned from day one. It should be designed for the consumer โ€” think about the developer experience first. Great APIs have excellent documentation, consistent error messages, and sensible defaults. Test your API by building something with it before releasing it.

When should I use microservices instead of a monolith?+

Start with a monolith. Microservices add significant operational complexity (service discovery, distributed tracing, network failures, eventual consistency). Extract services only when: (1) you have distinct business domains with different scaling needs, (2) different teams need to deploy independently without coordination, or (3) a specific component has wildly different tech requirements. Most teams adopt microservices too early. A well-structured monolith serves most startups and SMEs perfectly well.

How does JWT authentication work?+

When a user logs in, the server creates a JWT containing claims (user ID, roles, expiry), signs it with a secret key, and returns it. The client stores the JWT and sends it in the Authorization header (Bearer {token}) with every request. The server verifies the signature and reads the claims โ€” no database lookup needed. This makes JWTs stateless and scalable. The downside: you cannot invalidate a JWT before it expires, so keep access token lifetimes short (15โ€“60 minutes) and use refresh tokens for longer sessions.

What is the difference between OAuth2 and JWT?+

OAuth2 is an authorization framework โ€” a set of rules for delegating access. JWT is a token format โ€” a way to encode claims. They're complementary, not competing. OAuth2 typically uses JWTs as the format for its access tokens. You use OAuth2 when you want to let third-party apps access resources on behalf of a user (e.g., "Login with Google"). You use JWT when you need a compact, self-contained way to transmit verified claims between parties.

How do you scale APIs for millions of users?+

Scaling is layered. (1) Make your API stateless so you can run multiple instances behind a load balancer. (2) Add caching at every layer โ€” in-memory, Redis, CDN edge caches. (3) Optimize database queries, add indexes, and use read replicas. (4) Move heavy work to background jobs and queues. (5) Scale database with sharding or use managed services that auto-scale. (6) Use a CDN for static content and cacheable responses. (7) Monitor with distributed tracing to find bottlenecks. Measure before optimizing โ€” don't guess where the bottleneck is.

What is API Gateway and why is it needed?+

An API Gateway is the single entry point for all client requests to your backend. It handles cross-cutting concerns: authentication, rate limiting, SSL termination, routing, logging, and request/response transformation. Without a gateway, every microservice must implement these concerns independently โ€” creating inconsistency and duplication. The gateway lets each service focus purely on its domain logic. It also provides a stable interface to clients, even as internal services change and evolve.

How do you prevent API abuse?+

Layered defenses: (1) Rate limiting โ€” token bucket or sliding window, per IP and per user. (2) API keys โ€” identify and throttle consumers. (3) CAPTCHA or proof-of-work for public-facing forms. (4) IP allowlisting for internal APIs. (5) Anomaly detection โ€” alert on traffic spikes or unusual patterns. (6) Input validation โ€” reject malformed requests early. (7) Web Application Firewall (WAF) at the edge for known attack patterns. (8) Idempotency keys to prevent duplicate payment attacks.

What is CQRS and when should it be used?+

CQRS (Command Query Responsibility Segregation) separates read operations (queries) from write operations (commands). Reads and writes have different performance profiles, validation rules, and data shapes. By separating them, you can optimize each independently โ€” use denormalized read models for fast queries, normalized write models for data integrity. Use CQRS when your application has complex business logic, very different read/write scaling needs, or when you're implementing Event Sourcing. Avoid it for simple CRUD โ€” the overhead isn't worth it.

How do distributed systems communicate?+

Two patterns: synchronous and asynchronous. Synchronous: one service calls another via HTTP REST or gRPC and waits for a response. Simple, but creates temporal coupling โ€” if the downstream service is slow or down, the caller is affected. Asynchronous: services publish events to a message broker (RabbitMQ, Kafka, Azure Service Bus). Consumers process messages independently. More resilient but harder to debug and reason about. For read-heavy, real-time data: synchronous. For workflows, audit trails, and high-volume event processing: asynchronous.

What is caching and why is Redis popular?+

Caching stores computed results so future requests can be served without recomputation. Redis is popular because it's blazingly fast (microsecond reads from memory), supports rich data structures (strings, lists, sets, hashes, sorted sets), has built-in expiry (TTL), supports pub/sub for real-time features, and is battle-tested at massive scale. Redis serves as a cache, session store, rate limiter, leaderboard engine, and message broker โ€” making it the Swiss Army knife of backend infrastructure.

How do you design APIs for mobile applications?+

Mobile APIs have unique constraints: unreliable connections, limited bandwidth, battery consumption, and variable screen sizes needing different data. Key practices: (1) Support pagination โ€” never return unbounded lists. (2) Allow field selection โ€” mobile needs less data than web. (3) Use compression (gzip/brotli) for all responses. (4) Design for offline โ€” idempotent operations, sync endpoints, conflict resolution. (5) Version aggressively โ€” you can't force users to update apps. (6) Return minimal data by default with expand options for details.

What is idempotency and why does it matter?+

An operation is idempotent if performing it multiple times has the same effect as performing it once. It matters because network failures cause retries โ€” if your payment endpoint isn't idempotent, a retry after a timeout could charge the user twice. Implement idempotency with unique keys: the client sends a UUID in the request header, the server caches the result under that key, and returns the cached result for duplicate requests. GET, PUT, and DELETE are naturally idempotent. POST requires explicit handling.

What is the N+1 query problem?+

The N+1 problem occurs when you load a list of N items, then execute an additional database query for each item to load related data. If you have 100 orders and load each user separately, that's 101 queries instead of 1. It's insidious because it's not obvious in code and only becomes a problem at scale. Solutions: eager loading (SQL JOIN to load related data in one query), batch loading (DataLoader pattern), or denormalization. Use an ORM profiler in development to catch N+1 issues before they reach production.

What is the Repository Pattern and why use it?+

The Repository Pattern abstracts data access behind an interface. Instead of your service calling the ORM directly, it calls IUserRepository. Benefits: (1) Your business logic doesn't depend on a specific ORM โ€” swap databases without touching services. (2) Testability โ€” mock the repository interface in unit tests without needing a real database. (3) Centralized query logic โ€” your queries live in one place, not scattered across the codebase. Downside: adds a layer of indirection. For simple CRUD apps, it may be overkill.

What HTTP status code should I return for validation errors?+

Use 422 Unprocessable Entity for business/semantic validation failures (email already taken, insufficient funds), and 400 Bad Request for structural/syntactic failures (invalid JSON, missing required field). The key distinction: 400 means "I couldn't parse your request," 422 means "I understood your request but it violates business rules." Never return 200 with an error in the body โ€” this breaks API client logic and monitoring tools that check status codes.

What is eventual consistency and when do I need to worry about it?+

Eventual consistency means data will become consistent across all nodes "eventually" โ€” but there may be a window where different services see different versions of truth. In microservices, when Service A updates its database and publishes an event, Service B's database won't reflect that change until it processes the event โ€” which might be milliseconds or seconds later. This is fine for non-critical reads but problematic for operations requiring strong consistency (like financial transactions). Design for it by identifying which operations need strong consistency and handling compensating transactions for failures.

How do I handle file uploads in an API?+

For small files (<10MB): accept multipart/form-data directly. For large files: use pre-signed URLs โ€” the client requests a signed upload URL from your API, then uploads directly to blob storage (Azure Blob, S3), bypassing your servers entirely. This is more scalable, cheaper, and avoids request timeouts. Validate file types by magic bytes (not just extension), enforce size limits, scan for malware, and generate new random filenames before storing to prevent path traversal and enumeration attacks.

What is the difference between authentication and authorization?+

Authentication (AuthN) answers "Who are you?" โ€” verifying identity via credentials (username/password, OAuth token, certificate). Authorization (AuthZ) answers "What are you allowed to do?" โ€” checking permissions after identity is established. Authentication always comes first. You can be authenticated but not authorized (a logged-in user accessing an admin page). Common mistake: conflating them or using 401 Unauthorized when you should use 403 Forbidden. 401 means "you need to authenticate first." 403 means "you're authenticated but don't have permission."

What is CORS and how do I configure it?+

Cross-Origin Resource Sharing (CORS) is a browser security mechanism that blocks JavaScript from making requests to a different domain than the page it's on. When a browser makes a cross-origin request, the server must include CORS headers (Access-Control-Allow-Origin) permitting it. In production, never use wildcard (*) โ€” explicitly list your allowed origins. CORS only affects browser requests; server-to-server calls are not restricted by CORS. Configure restrictive CORS in your framework with explicit allowed origins per environment.

What is the difference between PUT and PATCH?+

PUT replaces the entire resource. If you PUT a user with only {name: "John"}, any fields you omit (email, phone, etc.) get cleared or set to defaults. PATCH applies a partial update โ€” only the fields you include are modified. Use PUT when you always send the complete resource, PATCH when you want to update specific fields without affecting others. PATCH with JSON Patch (RFC 6902) is the most expressive format, but for most APIs, simply accepting a partial object in PATCH is simpler and sufficient.

How do you test APIs effectively?+

Three layers: (1) Unit tests โ€” test service and domain logic in isolation with mocked dependencies. (2) Integration tests โ€” test your API endpoints against a real database using your framework's test utilities. These catch middleware, routing, and serialization issues that unit tests miss. (3) Contract tests โ€” verify your API behavior matches its OpenAPI specification. Tools: Jest/Vitest, pytest, PHPUnit, or xUnit for unit tests, Testcontainers for integration tests with real Docker databases, and Pact for consumer-driven contract testing in microservices.

What is API throttling vs rate limiting?+

Rate limiting sets a hard cap โ€” exceed it and you get 429 Too Many Requests. Throttling (or throttling / traffic shaping) slows down requests rather than rejecting them โ€” requests are queued or delayed when limits are approached. Rate limiting protects your infrastructure from abuse. Throttling provides a smoother degradation experience. In practice, most systems implement rate limiting with a clear Retry-After header so clients know when to back off. Include rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) in every response.

Should I use async/await everywhere in my API?+

Yes, for I/O-bound operations. When a thread awaits an async operation (database call, HTTP request, file read), the runtime returns that thread to pool to serve other requests. Under load, this dramatically increases throughput. Blocking a thread synchronously can cause thread pool starvation โ€” requests queue up waiting for threads even though the CPU is idle. Node.js is inherently async. In Python (FastAPI), use async def. In PHP, async is limited but frameworks like Swoole/ReactPHP enable it. In .NET, use async/await throughout. In practice: everything touching a database, file system, or network should be async.

How do I choose between SQL Server, PostgreSQL, and MySQL?+

PostgreSQL is the community's top choice for new projects โ€” excellent standards compliance, advanced features (JSON support, full-text search, window functions, extensible types), and it's completely free. SQL Server excels in enterprise environments with tight Azure integration, excellent tooling (SSMS), and features like columnstore indexes for analytics. MySQL is widely deployed and familiar but lags behind PostgreSQL in features and standards compliance. Choose based on your workload's requirements, team expertise, and cloud provider ecosystem โ€” not just familiarity.


// 13 โ€” Learning Path

API Architect Learning Roadmap

A structured 6-stage path from writing your first REST endpoint to designing systems that serve millions of users.

STAGE 01 API Fundamentals 4โ€“6 weeks

Master REST principles, HTTP methods and status codes, resource naming, API versioning, pagination, filtering, and idempotency. Build several small APIs from scratch without frameworks to understand the fundamentals.

REST DesignHTTP ProtocolJSON/OpenAPIPostman
STAGE 02 Backend Architecture 6โ€“8 weeks

Learn Clean Architecture, Dependency Injection, Repository Pattern, Service Layer, DTOs, and middleware pipelines in Express/Fastify. Build a production-structured project with proper error handling, validation (Zod), and TypeScript.

Express.jsTypeORMClean ArchitectureUnit Testing

Learn Clean Architecture, Dependency Injection, Repository Pattern, Service Layer, DTOs, and middleware pipelines in FastAPI. Build a production-structured project with proper error handling, validation (Pydantic), and async Python.

FastAPISQLAlchemyClean ArchitectureUnit Testing

Learn Clean Architecture, Dependency Injection, Repository Pattern, Service Layer, DTOs, and middleware pipelines in Laravel. Build a production-structured project with proper error handling, validation (Form Requests), and Eloquent ORM.

LaravelEloquentClean ArchitectureUnit Testing

Learn Clean Architecture, Dependency Injection, Repository Pattern, Service Layer, DTOs, and middleware pipelines in ASP.NET Core. Build a production-structured project with proper error handling, validation (FluentValidation), and object mapping (AutoMapper).

ASP.NET CoreEF CoreClean ArchitectureUnit Testing
STAGE 03 Security & Authentication 3โ€“4 weeks

Implement JWT authentication, OAuth2, role-based and claims-based authorization. Learn OWASP API top threats, input validation, rate limiting, CORS, and security headers. Build an auth service with refresh token rotation.

JWTOAuth2OWASPRate Limiting
STAGE 04 Microservices & Distributed Systems 8โ€“10 weeks

Understand microservice decomposition, API Gateway patterns, service-to-service communication (HTTP, gRPC, message buses), CQRS, Event Sourcing, and the Saga pattern for distributed transactions. Build a mini e-commerce system with 3โ€“4 services.

MicroservicesRabbitMQ/KafkagRPCCQRSDocker
STAGE 05 Cloud, DevOps & Observability 4โ€“6 weeks

Learn Docker, Kubernetes basics, CI/CD pipelines with GitHub Actions or Azure DevOps, infrastructure as code (Terraform or Bicep), structured logging with Serilog, distributed tracing, and Application Insights. Deploy your system to Azure or AWS.

KubernetesGitHub ActionsTerraformAzureObservability
STAGE 06 System Design & Architecture Leadership Ongoing

Study real-world system design cases (Twitter, Uber, Netflix). Practice Architecture Decision Records (ADRs). Learn capacity planning, SLA/SLO/SLI definitions, chaos engineering, and disaster recovery. Read: Designing Data-Intensive Applications (Kleppmann), Building Microservices (Newman), System Design Interview (Xu).

System DesignADRsSRE PracticesCapacity Planning
โœ“ Pro Tip โ€” Portfolio Projects

Don't just study โ€” build. A GitHub portfolio with a well-structured API project using Clean Architecture, JWT auth, Docker, CI/CD, and Swagger documentation will outshine any certification. Document your architectural decisions in a README. Share it.