Skip to content
Back to Project Tutorials

API Service

Design, build, and deploy a production-grade API with auth, rate limiting, and monitoring.

API Design

Define endpoints, data contracts, auth model, and API versioning strategy.

Why

API design is a contract that outlives your implementation. Once external developers depend on your endpoints, breaking changes cost them days of rework and cost you trust. Good design from the start means years of pain-free evolution.

When

Before writing any endpoint code. Typically 1-2 weeks for a well-scoped API. Design reviews with 2-3 engineers catch 80% of design flaws.

How

1. Define resources and relationships - List all nouns in your domain (User, Order, Product) - Map relationships: one-to-many, many-to-many - Choose REST or GraphQL based on client needs 2. Design URL structure (REST) - Resource-based: /api/v1/users, /api/v1/orders - Nested for sub-resources: /api/v1/users/:id/orders - Plural nouns, kebab-case, max 2 levels of nesting 3. Define data contracts - Use OpenAPI/Swagger to document every endpoint - Specify request body, params, query, response, errors - Use consistent naming: snake_case or camelCase (pick one, stick to it) - Version from day 1: /api/v1/... 4. Design authentication - API keys for server-to-server (simple, revocable) - OAuth 2.0 for third-party access (complex but standard) - JWT for stateless auth (short expiry + refresh token) - Never pass auth in URL params (use headers) 5. Plan for evolution - Additive changes only (new fields, new endpoints) — never remove - Deprecation: mark old fields, return warning header, remove after 6 months - Versioning: URL-based (/v1/, /v2/) for breaking changes

Example

OpenAPI spec sketch: GET /api/v1/users — list (paginated) POST /api/v1/users — create GET /api/v1/users/:id — get one PATCH /api/v1/users/:id — partial update DELETE /api/v1/users/:id — delete (soft) GET /api/v1/users/:id/orders — list user's orders Auth: Bearer token in Authorization header Authorization: Bearer <api_key> Response format: { "data": {...}, "meta": { "page": 1, "total": 42 } } Error format: { "error": { "code": "NOT_FOUND", "message": "..." } }

Common Problems

  • ⚠Inconsistent naming: camelCase in some endpoints, snake_case in others
  • ⚠No versioning: first breaking change alienates all existing clients
  • ⚠Verbs in URLs: /api/getUser instead of /api/users/:id — breaks REST conventions
  • ⚠Leaking internal IDs: sequential IDs expose user count and enable enumeration attacks

Best Practices

  • ✓Write the OpenAPI spec before coding — it's the contract and the docs
  • ✓Use UUIDs for all public IDs — no sequential integers
  • ✓Return consistent error shapes: { error: { code, message, details? } }
  • ✓Version in the URL (/v1/) not in headers — it's visible and cacheable

Implementation

Build endpoints with validation, error handling, rate limiting, and tests.

Why

API implementation quality directly determines uptime, security, and developer experience. A single unvalidated input can cause a data breach. A missing rate limit can cause a DDoS. These are not theoretical risks — they are the most common causes of API incidents.

When

After design is approved. Typically 4-8 weeks for a 20-30 endpoint API. Build auth and rate limiting first — they're cross-cutting and hard to retrofit.

How

1. Set up the framework and middleware - Choose framework: Fastify (Node), FastAPI (Python), Gin (Go) - Add middleware: CORS, request logging, error handler - Set up OpenAPI validation middleware (e.g., zod-to-openapi) 2. Implement auth and rate limiting first - Auth middleware: validate Bearer token, attach user to request - Rate limit: 100 req/min per API key (use Redis for distributed) - Return 429 with Retry-After header when exceeded 3. Build endpoints in dependency order - CRUD for each resource: list, get, create, update, delete - Validate input with zod/joi — reject early, fail fast - Use transactions for multi-step writes - Return consistent response shapes 4. Handle errors properly - 400 for validation errors (with field details) - 401 for missing/invalid auth - 403 for insufficient permissions - 404 for not found (don't leak existence) - 500 for unexpected errors (log full stack, return generic message) 5. Write tests for every endpoint - Happy path + validation errors + auth failures + edge cases - Integration tests with real database (test transaction rollback) - Load test critical endpoints with k6 or Artillery

Example

Fastify endpoint with validation: server.post('/api/v1/users', { schema: { body: zodToJsonSchema(userCreateSchema), }, preHandler: [authMiddleware, rateLimit], }, async (request, reply) => { const user = await db.user.create({ data: request.body, }); return reply.code(201).send({ data: user }); }); Rate limit middleware (Redis-backed): const limited = await redis.incr(`rl:${apiKey}`); if (limited > 100) { return reply.code(429).header('Retry-After', '60').send({ error: { code: 'RATE_LIMITED', message: 'Too many requests' } }); }

Common Problems

  • ⚠No input validation — trusting client data leads to SQL injection and data corruption
  • ⚠Inconsistent error responses — clients can't programmatically handle errors
  • ⚠No rate limiting — one client can exhaust database connections for everyone
  • ⚠N+1 queries: listing endpoints that fetch related data per item instead of in bulk

Best Practices

  • ✓Validate every input with a schema library (zod/joi) — never trust client data
  • ✓Use consistent error envelope: { error: { code, message, details? } }
  • ✓Add rate limiting on day 1 — it's a security feature, not a feature request
  • ✓Use DataLoader or batch queries to eliminate N+1 patterns

Deployment & Monitoring

Deploy to production with zero-downtime, monitoring, alerts, and runbooks.

Why

API deployment is where code meets reality. Without proper monitoring, you're flying blind — the first sign of trouble is a customer complaint. Without zero-downtime deploys, every release risks dropping in-flight requests. Without runbooks, 3 AM incidents become multi-hour outages.

When

When implementation is tested and staged. Set up monitoring before the first production deploy — you need baseline metrics to detect anomalies.

How

1. Choose deployment platform - Container: Docker + Fly.io / Railway / AWS ECS - Serverless: Vercel / Cloudflare Workers (for edge APIs) - VM: Docker Compose on a VPS (simplest, least scalable) 2. Set up CI/CD pipeline - Build Docker image on tag - Push to registry - Deploy to staging, run smoke tests - Manual promote to production 3. Configure zero-downtime deploy - Health check endpoint: GET /health returns 200 - Rolling update: new instances start, old ones drain - Database migrations: run before app starts, backward-compatible 4. Set up monitoring and alerts - Uptime: ping /health every 30s, alert on 3 consecutive failures - Latency: p50, p95, p99 per endpoint (Datadog/Grafana) - Error rate: alert if > 1% of requests are 5xx - Saturation: CPU, memory, DB connections, queue depth 5. Write runbooks for common incidents - Database down: failover steps, who to call - High error rate: how to rollback, how to read logs - Rate limit exceeded: how to identify the client, how to adjust - Keep runbooks in the repo, next to the code

Example

Dockerfile (multi-stage): FROM node:20 AS build COPY . . && RUN npm ci && npm run build FROM node:20-slim COPY --from=build /app/dist ./dist CMD ["node", "dist/server.js"] Health check endpoint: GET /health -> 200 { "status": "ok", "db": "connected" } Deploy pipeline: git tag v1.2.0 -> build image -> push to registry -> deploy to staging -> run smoke tests -> manual approve -> rolling deploy to production -> monitor error rate for 10 min Alerts (PagerDuty): - uptime < 99.5% in 5 min window - p95 latency > 500ms for 10 min - 5xx error rate > 1% for 5 min - DB connection pool > 80% saturated

Common Problems

  • ⚠No health check — load balancer can't know if the app is ready to receive traffic
  • ⚠Deploying without migrations running — new code expects new schema, crashes on startup
  • ⚠Alert fatigue: too many low-priority alerts -> engineers ignore PagerDuty
  • ⚠No rollback plan: bad deploy at 3 PM, team scrambles to revert for 2 hours

Best Practices

  • ✓Health check endpoint from day 1 — /health returns DB + cache + queue status
  • ✓Run migrations before app starts, make them backward-compatible
  • ✓Keep alerts actionable: each alert has a runbook link in the description
  • ✓Practice rollbacks in staging — if you can't roll back in staging, you can't in prod

Full Developer Lifecycle

A 14-stage developer roadmap from idea to maintenance — visualize dependencies and track progress.

Next Step