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 usersGET /api/v1/users/{id} โ get specific userPOST /api/v1/orders โ create new orderPUT /api/v1/products/{id} โ replace productPATCH /api/v1/products/{id} โ partial updateDELETE /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// FilteringGET /api/v1/users?status=active&role=admin&country=US// Sorting (prefix - for descending)GET /api/v1/users?sort=-createdAt,lastName// Field selection / sparse fieldsetsGET /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
from fastapi importDependsfrom sqlalchemy.ext.asyncio importAsyncSessionfrom app.core.database importget_sessionfrom app.repositories.user_repository importUserRepositoryfrom app.services.user_service importUserServiceasync defget_user_repository(
session: AsyncSession = Depends(get_session)
) -> UserRepository:
returnUserRepository(session)
async defget_user_service(
repo: UserRepository = Depends(get_user_repository),
cache: CacheService = Depends(CacheService)
) -> UserService:
returnUserService(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 functionregister(): 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
// app/Repositories/Interfaces/IUserRepository.phpinterfaceIUserRepository {
public functionfindById(string$id): ?User;
public functionfindAll(UserFilter$filter): Collection;
public functioncreate(array$data): User;
public functionupdate(User$user, array$data): User;
public functiondelete(string$id): void;
}
// app/Repositories/UserRepository.phpclassUserRepositoryimplementsIUserRepository {
public function__construct(privateUser$model) {}
public functionfindById(string$id): ?User {
return$this->model->find($id);
}
}
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
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.
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
NginxKongAzure API ManagementAWS 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
asyncgetUserById(userId: string): Promise<UserDto> {
const cacheKey = `user:${userId}`;
// 1. Try cache firstconst cached = await redis.get(cacheKey);
if (cached) returnJSON.parse(cached);
// 2. Cache miss โ query databaseconst user = await userRepository.findOne({ where: { id: userId } });
if (!user) throw newNotFoundError(`User ${userId} not found`);
// 3. Store in cache with TTL (15 minutes)await redis.setex(cacheKey, 900, JSON.stringify(user));
returntoUserDto(user);
}
Python โ Redis
Caching Pattern
async defget_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:
returnUserDto.model_validate_json(cached)
# 2. Cache miss โ query database
user = await self.repo.get_by_id(user_id)
if not user:
raiseNotFoundException(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 functiongetUser(string$userId): UserDto
{
$cacheKey = "user:{$userId}";
// Cache::remember handles try-cache + fallback-to-dbreturnCache::remember($cacheKey, now()->addMinutes(15),
function () use ($userId) {
$user = $this->repository->findById($userId);
if (!$user) {
throw newModelNotFoundException("User not
found");
}
// Automatically cached for 15 minutesreturnUserResource::make($user);
}
);
}
C# โ Redis Caching
Pattern
public asyncTask<UserDto> GetUserAsync(Guid userId)
{
var cacheKey = $"user:{userId}";
// 1. Try cache firstvar cached = await _redis.GetStringAsync(cacheKey);
if (cached is not null)
return JsonSerializer.Deserialize<UserDto>(cached)!;
// 2. Cache miss โ query databasevar user = await _repo.GetByIdAsync(userId)
?? throw newNotFoundException($"User
{userId} not found");
// 3. Store in cache with TTLawait _redis.SetStringAsync(cacheKey,
JsonSerializer.Serialize(user),
newDistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15) });
return _mapper.Map<UserDto>(user);
}
Database Optimization
Indexing โ Add indexes on columns used in WHERE, JOIN, and ORDER BY. Composite indexes for
multi-column queries. Analyze query plans regularly.
Query Optimization โ Avoid SELECT *, use projections. Only fetch the columns you need.
Analyze slow query logs.
N+1 Problem โ Loading a list then querying each item individually. Fix with eager loading
(include/joinedload/with) or batch queries.
Connection Pooling โ Reuse database connections instead of creating new ones per request.
Configured by default in most ORMs and database drivers.
Read Replicas โ Offload heavy read traffic to replica databases. Write to primary, read from
replicas.
Async/Await Everywhere โ Never block threads. Use async database calls to maximize throughput
under load.
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 joinedloadfrom sqlalchemy.orm importjoinedload
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 ($ordersas$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 AppServiceProviderModel::preventLazyLoading(!app()->isProduction());
EF Core โ Avoiding
N+1 with Eager Loading
// โ BAD โ N+1 problem: 1 query for orders + N queries for each
uservar 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 JOINvar 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.
from fastapi importAPIRouter, 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 defcreate_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 functionstore(CreateUserRequest$request): JsonResponse
{
$user = $this->userService->create($request->validated());
return (newUserResource($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 asyncTask<IActionResult>
CreateUser([FromBody] CreateUserDto
request)
{
var result = await _userService.CreateAsync(request);
returnCreatedAtAction(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
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 managerasync 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 stockreturn$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 = newOrder { 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 effectsclassCreateOrderCommand {
constructor(
public userId: string,
public items: OrderItem[]
) {}
}
classCreateOrderHandler {
asynchandle(cmd: CreateOrderCommand):
Promise<string> {
const order = Order.create(cmd.userId,
cmd.items);
awaitthis.orderRepo.save(order);
awaitthis.eventBus.publish(newOrderCreatedEvent(order.id));
return order.id;
}
}
// Query โ pure read, no side effects, can be cachedclassGetOrderQuery {
constructor(public orderId: string) {}
}
// Controller dispatches to handler โ stays thinconst orderId = await mediator.send(newCreateOrderCommand(userId,
items));
Python โ CQRS
Dataclass Pattern
from dataclasses import dataclass
# Command โ write operation with side effects
@dataclass
classCreateOrderCommand:
user_id: UUID
items: list[OrderItem]
classCreateOrderHandler:
async defhandle(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
classGetOrderQuery:
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 effectsclassCreateOrderCommand {
public function__construct(
public readonly string$userId,
public readonly array$items
) {}
}
classCreateOrderHandler {
public functionhandle(CreateOrderCommand$cmd): string {
$order = Order::create($cmd->userId, $cmd->items);
$this->repo->save($order);
event(newOrderCreatedEvent($order->id));
return$order->id;
}
}
// Query โ pure read, no side effects, can be cachedclassGetOrderQuery {
public function__construct(public readonly
string$orderId) {}
}
// Controller dispatches to handler โ stays thin$orderId = app(Mediator::class)->send(newCreateOrderCommand($userId, $items));
C# โ MediatR CQRS
Pattern
// Command โ write operation with side effectspublic recordCreateOrderCommand(Guid
UserId, List<OrderItem> Items)
: IRequest<Guid>;
public classCreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
public asyncTask<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.UserId, cmd.Items);
await _repo.AddAsync(order, ct);
await _bus.PublishAsync(newOrderCreatedEvent(order.Id));
return order.Id;
}
}
// Query โ pure read, no side effects, can be cachedpublic recordGetOrderQuery(Guid
OrderId) : IRequest<OrderDto>;
// Controller dispatches to MediatR โ stays thinvar result = await _mediator.Send(newCreateOrderCommand(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 imageFROM 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 .
EXPOSE8080# Run as non-root for securityUSER node
CMD ["node", "dist/server.js"]
Dockerfile โ
Python / FastAPI Optimized
# Multi-stage build โ smaller final imageFROM 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 . .
EXPOSE8080# Run as non-root for securityRUN 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 imageFROM 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
EXPOSE9000# Run as non-root for securityUSER www-data
CMD ["php-fpm"]
Dockerfile โ
ASP.NET Core Optimized
# Multi-stage build โ smaller final imageFROM 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 .
EXPOSE8080# Run as non-root for securityUSER app
ENTRYPOINT ["dotnet", "MyApi.dll"]
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 chatbotPOST /api/ai/summarize โ document summarizationPOST /api/ai/sentiment โ sentiment analysisPOST /api/ai/recommendation โ personalized recommendationsPOST /api/ai/search โ semantic/vector searchPOST /api/ai/embeddings โ generate text embeddingsGET /api/ai/models โ list available models
public asyncTask<string> ChatAsync(string userMessage,
string systemPrompt)
{
var client = newOpenAIClient(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 asyncIAsyncEnumerable<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
Rate Limiting AI Endpoints โ AI calls are expensive. Apply aggressive rate limiting and user
quotas to prevent abuse and control costs.
Prompt Injection Defense โ Sanitize user input. Never concatenate raw user text directly into
system prompts without validation.
Streaming Responses โ LLMs are slow. Use Server-Sent Events to stream tokens as they generate
rather than waiting for full completion.
Caching AI Responses โ Cache identical prompts for deterministic endpoints like
classification. Saves cost and latency.
Fallback Strategies โ AI services go down. Implement circuit breakers and graceful fallbacks
for AI-augmented features.
// 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 01API Fundamentals4โ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 02Backend Architecture6โ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 03Security & Authentication3โ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.
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 05Cloud, DevOps & Observability4โ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.
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.