Back to Blog
Development

Building APIs That Scale: Design Principles From 100M Requests/Day

Learn the API design principles we use to build systems handling 100M+ requests daily. Real-world patterns for scalable, maintainable APIs.

John Smith
December 20, 2023
14 min read
#api#backend#scalability#architecture
/blog/api-design.jpg

Featured Image

The difference between an API that scales and one that breaks under load isn't luck—it's deliberate design. After building APIs handling 100M+ requests daily, we've learned the principles that separate good APIs from great ones.

Core Principle: Resource-Based Design

An API is a contract between client and server. A well-designed API makes that contract clear.

#Think in Resources, Not Actions

❌ Bad API Design: ``` POST /users/getUserById?id=123 POST /users/deleteUser?id=123 GET /getActiveUsers ```

✅ Good API Design: ``` GET /users/123 # Retrieve user 123 DELETE /users/123 # Delete user 123 GET /users?status=active # List active users ```

**Why this matters:** - Predictable URLs enable caching - Standard HTTP methods convey intent - Stateless design enables horizontal scaling - Easy to document and teach

#HTTP Methods: Use Them Correctly

| Method | Purpose | Idempotent | Safe | |--------|---------|-----------|------| | GET | Retrieve | ✓ | ✓ | | POST | Create | ✗ | ✗ | | PUT | Replace | ✓ | ✗ | | PATCH | Partial update | ✗ | ✗ | | DELETE | Delete | ✓ | ✗ |

**Idempotent** means calling it multiple times has the same effect as calling it once.

Example: ```javascript // ❌ Wrong - POST used for retrieval POST /users/search?query=john

// ✅ Right - GET used for retrieval GET /users?query=john

// ✅ Right - DELETE is idempotent (safe to retry) DELETE /users/123 DELETE /users/123 // Same result

// ❌ Wrong - POST is not idempotent (creates duplicate) POST /users {name: "John"} POST /users {name: "John"} // Creates second user ```

HTTP Status Codes: Be Explicit

Clients need to understand what happened. Use the right status code.

#Success (2xx) - **200 OK:** Request succeeded, body contains response - **201 Created:** Resource created, Location header contains new URL - **204 No Content:** Success, but no response body (DELETE often uses this)

#Redirection (3xx) - **301 Moved Permanently:** Resource moved, update bookmarks - **304 Not Modified:** Client has cached version, no need to download

#Client Error (4xx) - **400 Bad Request:** Malformed request (invalid JSON, missing fields) - **401 Unauthorized:** Missing authentication - **403 Forbidden:** Authenticated but not authorized - **404 Not Found:** Resource doesn't exist - **409 Conflict:** Request conflicts with current state - **422 Unprocessable Entity:** Validation failed (email already taken) - **429 Too Many Requests:** Rate limited

#Server Error (5xx) - **500 Internal Server Error:** Our fault, not client's - **503 Service Unavailable:** Temporarily down (database issue, deploying)

**Implementation example:** ```javascript app.post('/users', async (req, res) => { // Validate input if (!req.body.email) { return res.status(400).json({ error: 'email_required', message: 'Email is required' }) }

// Check if exists const existing = await User.findByEmail(req.body.email) if (existing) { return res.status(409).json({ error: 'email_exists', message: 'Email already registered' }) }

// Create const user = await User.create(req.body) res.status(201).json(user) }) ```

Pagination: Essential for Scale

Never return unlimited data. Ever.

```javascript // Request GET /users?limit=20&offset=0 GET /users?limit=20&cursor=abc123

// Response { data: [...], pagination: { limit: 20, offset: 0, total: 5000, hasMore: true } } ```

**Cursor-based pagination is better for scale:** - Offset-based requires counting all rows - Cursor-based uses database index directly - Offset-based breaks if rows are deleted - Cursor-based is stable even with data changes

Rate Limiting: Protect Your API

Rate limiting prevents abuse and controls load.

```javascript // Headers tell client about limits res.set('RateLimit-Limit', 1000) // 1000 requests res.set('RateLimit-Remaining', 999) // 999 left res.set('RateLimit-Reset', 1609459200) // Resets at this unix timestamp

// When exceeded if (remaining < 1) { res.status(429).json({ error: &apos;rate_limited&apos;, retryAfter: 3600 }) } ```

Versioning: Plan for Change

How you version your API will affect you for years.

#Strategy #1: URL Path Versioning ``` GET /api/v1/users GET /api/v2/users ```

Pros: Clear, cacheable Cons: Duplication, multiple implementations

#Strategy #2: Header Versioning ``` GET /api/users Header: Accept: application/vnd.company.v2+json ```

Pros: Single endpoint Cons: Less obvious, harder to debug

**Our recommendation:** URL versioning is clearer and easier to support. Start with v1, plan for v2, migrate users gradually over 1-2 years.

**Critical rule:** Never break v1 until 100% of users have migrated.

Error Responses: Be Helpful

When things go wrong, help developers understand why.

```javascript // ❌ Unhelpful { error: &apos;Invalid request&apos; }

// ✅ Helpful { error: &apos;validation_failed&apos;, message: &apos;Invalid request body&apos;, details: { email: &apos;Email already registered&apos;, password: &apos;Password must be at least 8 characters&apos; }, timestamp: &apos;2024-01-15T10:30:00Z&apos;, requestId: &apos;req_abc123&apos; // For support debugging } ```

Performance: Cache Aggressively

Caching is often the difference between slow and fast APIs.

```javascript // Set cache headers for GET requests app.get(&apos;/posts/:id&apos;, (req, res) => { // Cache for 1 hour res.set(&apos;Cache-Control&apos;, &apos;public, max-age=3600&apos;) res.json(post) })

// Use ETags for conditional requests const eTag = hash(post) res.set(&apos;ETag&apos;, eTag)

// Client sends If-None-Match if (req.headers[&apos;if-none-match&apos;] === eTag) { res.status(304).end() // Not modified } ```

Security: Non-Negotiable

#Authentication ```javascript // JWT is simple and scalable const token = jwt.sign({ userId: 123 }, secret)

// Validate on every request app.use((req, res, next) => { const token = req.headers.authorization?.replace(&apos;Bearer &apos;, &apos;&apos;) if (!token) { return res.status(401).json({ error: &apos;unauthorized&apos; }) } try { req.user = jwt.verify(token, secret) next() } catch (e) { res.status(401).json({ error: &apos;invalid_token&apos; }) } }) ```

#Input Validation ```javascript // Never trust client input const email = req.body.email?.toLowerCase().trim() const password = req.body.password

if (!email || !password) { return res.status(400).json({ error: &apos;missing_fields&apos; }) }

// Validate format if (!email.match(/.+@.+\..+/)) { return res.status(422).json({ error: &apos;invalid_email&apos; }) } ```

#Use HTTPS Always Every API should require HTTPS. No exceptions.

Monitoring: Know When Things Break

```javascript app.use((req, res, next) => { const start = Date.now() res.on(&apos;finish&apos;, () => { const duration = Date.now() - start monitor.recordRequest({ method: req.method, path: req.path, status: res.statusCode, duration, userId: req.user?.id }) }) next() }) ```

Track: - Request count by endpoint - Response time (p50, p95, p99) - Error rate by status code - Database query times

Your API Design Checklist

Before shipping: - ✓ Resource-based URL structure - ✓ Correct HTTP methods and status codes - ✓ Pagination for lists - ✓ Rate limiting configured - ✓ Comprehensive error responses - ✓ Caching headers for GET requests - ✓ Authentication on protected routes - ✓ Input validation - ✓ Monitoring and alerting - ✓ API documentation (OpenAPI/Swagger)

Well-designed APIs scale with your business and remain maintainable for years. Take the time to design right.

Related Articles

DE
Development

React Performance Optimization: A Complete Guide from 3s to 800ms Load Time

Master React performance optimization techniques to achieve sub-second load times. Learn strategies we used to improve load times by 73% on production apps.

Read More

Ready to Bring Your Ideas to Life?

Let's discuss how we can help with your next digital project.

Get Started