Backend & APIs
The backend is the engine room of every modern application. It receives client requests, enforces business rules, persists state, communicates with external services, and returns structured responses. Whether you are building a mobile app, a single-page web application, or an internal microservice, the quality of your backend architecture determines scalability, security, and developer velocity.
APIs are the contracts that connect backends to the outside world. A well-designed API hides implementation complexity, exposes predictable interfaces, and evolves without breaking consumers. This section teaches you how to design, implement, and operate production-grade backends and APIs using patterns that survive real-world traffic.
The topics in this section cover the full lifecycle: resource modeling with REST, flexible querying with GraphQL, end-to-end type safety with tRPC, event-driven integration with webhooks, specification-driven design with OpenAPI, traffic control with rate limiting, identity with authentication, access control with authorization, and efficient data retrieval with pagination.
A backend is any software that runs on a server and provides services to clients. It typically includes an HTTP server, application logic, data access layers, integrations with databases and third-party services, and operational concerns such as logging, monitoring, and security. Unlike frontend code, backend code executes in a controlled environment that you own, which gives you more power and more responsibility.
Backend responsibilities can be grouped into four layers. The transport layer handles incoming connections and protocol concerns. The application layer contains business logic and orchestration. The data layer manages persistence, caching, and retrieval. The integration layer communicates with external APIs, message queues, and event streams. Keeping these layers distinct makes systems easier to test, refactor, and scale.
| Layer | Responsibilities | Examples |
|---|---|---|
| Transport | Routing, parsing, serialization | Express, Fastify, Hono |
| Application | Business rules, workflows | Services, use cases, validators |
| Data | Persistence, caching, search | PostgreSQL, Redis, Prisma |
| Integration | External APIs, events, queues | Kafka, SQS, webhooks, gRPC |
info
Every API request travels through a predictable pipeline. Understanding this lifecycle helps you place security, observability, and error handling at the right points. A typical request starts at the client, transits DNS and a load balancer, reaches an application server, executes business logic, queries a database, and returns a response.
Inside the server, middleware runs first. Middleware handles cross-cutting concerns such as authentication, rate limiting, CORS, request logging, and request ID assignment. After middleware, the router dispatches the request to a handler. The handler validates input, calls services, coordinates database transactions, and formats the response. Finally, the transport serializes the response and sends it back to the client.
| 1 | import express from "express"; |
| 2 | import { v4 as uuid } from "uuid"; |
| 3 | |
| 4 | const app = express(); |
| 5 | |
| 6 | // 1. Middleware layer: runs on every request |
| 7 | app.use(express.json()); |
| 8 | app.use((req, res, next) => { |
| 9 | req.id = uuid(); |
| 10 | res.setHeader("X-Request-Id", req.id); |
| 11 | console.log(`[${req.id}] ${req.method} ${req.path}`); |
| 12 | next(); |
| 13 | }); |
| 14 | |
| 15 | // 2. Authentication middleware |
| 16 | app.use(authenticateRequest); |
| 17 | |
| 18 | // 3. Rate limiting middleware |
| 19 | app.use(rateLimiter); |
| 20 | |
| 21 | // 4. Route handler |
| 22 | app.get("/api/users/:id", async (req, res, next) => { |
| 23 | try { |
| 24 | const user = await userService.findById(req.params.id); |
| 25 | if (!user) { |
| 26 | return res.status(404).json({ error: "User not found" }); |
| 27 | } |
| 28 | return res.json({ data: user }); |
| 29 | } catch (err) { |
| 30 | return next(err); |
| 31 | } |
| 32 | }); |
| 33 | |
| 34 | // 5. Error handling middleware |
| 35 | app.use((err, req, res, next) => { |
| 36 | console.error(`[${req.id}]`, err); |
| 37 | res.status(err.status || 500).json({ |
| 38 | error: err.message || "Internal server error", |
| 39 | requestId: req.id, |
| 40 | }); |
| 41 | }); |
best practice
Modern backends expose data through several API styles. REST is the most common, relying on resources, HTTP verbs, and stateless interactions. GraphQL gives clients control over the shape of the response. tRPC provides end-to-end type safety for TypeScript monorepos. gRPC excels in internal microservice communication with high-performance binary payloads. Webhooks enable event-driven push notifications from one system to another.
| Paradigm | Best For | Tradeoff |
|---|---|---|
| REST | Public APIs, caching, CRUD | Over-fetching, multiple round trips |
| GraphQL | Flexible frontends, mobile apps | Complexity, caching challenges |
| tRPC | TypeScript full-stack teams | Tightly coupled to TypeScript |
| gRPC | Internal microservices | Browser support requires proxy |
| Webhooks | Event-driven integrations | Delivery reliability concerns |
note
Good backend design is measured by clarity, reliability, and maintainability. A few principles apply across every paradigm. Start with explicit contracts. Consumers should know what endpoints or procedures exist, what inputs they accept, and what outputs they return. Use standard HTTP semantics when appropriate and consistent naming conventions everywhere.
Fail predictably. Every error should include a machine-readable code, a human-readable message, and enough context to debug the issue. Return appropriate status codes, never expose internal stack traces to clients, and log full details server-side. Design for retries by making mutations idempotent whenever possible.
Security and performance are not afterthoughts. Authenticate and authorize before processing sensitive operations. Validate and sanitize all input. Use parameterized queries to prevent injection. Cache aggressively but invalidate carefully. Measure latency, throughput, and error rates from day one.
| 1 | // Consistent error shape used across the API |
| 2 | interface ApiError { |
| 3 | code: string; |
| 4 | message: string; |
| 5 | details?: Record<string, unknown>; |
| 6 | requestId: string; |
| 7 | } |
| 8 | |
| 9 | function createError( |
| 10 | code: string, |
| 11 | message: string, |
| 12 | requestId: string, |
| 13 | details?: Record<string, unknown> |
| 14 | ): ApiError { |
| 15 | return { code, message, details, requestId }; |
| 16 | } |
| 17 | |
| 18 | // Example responses |
| 19 | // 400 Bad Request |
| 20 | { |
| 21 | "code": "VALIDATION_ERROR", |
| 22 | "message": "Email is required", |
| 23 | "details": { "field": "email" }, |
| 24 | "requestId": "req_abc123" |
| 25 | } |
| 26 | |
| 27 | // 409 Conflict |
| 28 | { |
| 29 | "code": "RESOURCE_ALREADY_EXISTS", |
| 30 | "message": "A user with this email already exists", |
| 31 | "requestId": "req_abc123" |
| 32 | } |
| 33 | |
| 34 | // 500 Internal Server Error |
| 35 | { |
| 36 | "code": "INTERNAL_ERROR", |
| 37 | "message": "Something went wrong. Please try again.", |
| 38 | "requestId": "req_abc123" |
| 39 | } |
best practice
Each topic in this section focuses on a specific backend concern. The guides are designed to be read in order or used as standalone references when you need to solve a particular problem.
REST API Design
Resources, HTTP methods, status codes, versioning, HATEOAS, and anti-patterns.
GraphQL
Schemas, resolvers, mutations, subscriptions, and the N+1 problem.
tRPC
Type-safe routers, procedures, context, middleware, and when to use them.
Webhooks
HMAC signatures, retries, idempotency, verification, and delivery guarantees.
OpenAPI
Spec structure, generating docs, code generation, and documentation UI.
Rate Limiting
Token bucket, sliding window, leaky bucket, and standard headers.
Authentication
JWT, sessions, OAuth2, SSO, MFA, and passwordless flows.
Authorization
RBAC, ABAC, claims, policies, and ownership checks.
Pagination
Offset, cursor, keyset, search-after, and performance tradeoffs.
pro tip
Building the API is only half the work. Operating it reliably requires observability, graceful degradation, and safe deployments. Log structured events with severity levels and correlation IDs. Emit metrics for request count, latency percentiles, and error rates. Use health checks and readiness probes so load balancers can route around unhealthy instances.
Deploy changes incrementally. Canary releases, feature flags, and database migration strategies reduce the blast radius of bad changes. Always test against production-like data volumes. A query that is fast with ten rows can be catastrophic with ten million.
| 1 | // Structured logging example |
| 2 | import { logger } from "./logger"; |
| 3 | |
| 4 | app.use((req, res, next) => { |
| 5 | const start = Date.now(); |
| 6 | res.on("finish", () => { |
| 7 | logger.info({ |
| 8 | message: "request completed", |
| 9 | method: req.method, |
| 10 | path: req.path, |
| 11 | statusCode: res.statusCode, |
| 12 | durationMs: Date.now() - start, |
| 13 | requestId: req.id, |
| 14 | userAgent: req.get("user-agent"), |
| 15 | }); |
| 16 | }); |
| 17 | next(); |
| 18 | }); |
warning