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.
Learn the API design principles we use to build systems handling 100M+ requests daily. Real-world patterns for scalable, maintainable APIs.
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.
An API is a contract between client and server. A well-designed API makes that contract clear.
❌ 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
| 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 ```
Clients need to understand what happened. Use the right status code.
**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) }) ```
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 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: 'rate_limited', retryAfter: 3600 }) } ```
How you version your API will affect you for years.
Pros: Clear, cacheable Cons: Duplication, multiple implementations
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.
When things go wrong, help developers understand why.
```javascript // ❌ Unhelpful { error: 'Invalid request' }
// ✅ Helpful { error: 'validation_failed', message: 'Invalid request body', details: { email: 'Email already registered', password: 'Password must be at least 8 characters' }, timestamp: '2024-01-15T10:30:00Z', requestId: 'req_abc123' // For support debugging } ```
Caching is often the difference between slow and fast APIs.
```javascript // Set cache headers for GET requests app.get('/posts/:id', (req, res) => { // Cache for 1 hour res.set('Cache-Control', 'public, max-age=3600') res.json(post) })
// Use ETags for conditional requests const eTag = hash(post) res.set('ETag', eTag)
// Client sends If-None-Match if (req.headers['if-none-match'] === eTag) { res.status(304).end() // Not modified } ```
// Validate on every request app.use((req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', '') if (!token) { return res.status(401).json({ error: 'unauthorized' }) } try { req.user = jwt.verify(token, secret) next() } catch (e) { res.status(401).json({ error: 'invalid_token' }) } }) ```
if (!email || !password) { return res.status(400).json({ error: 'missing_fields' }) }
// Validate format if (!email.match(/.+@.+\..+/)) { return res.status(422).json({ error: 'invalid_email' }) } ```
```javascript app.use((req, res, next) => { const start = Date.now() res.on('finish', () => { 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
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.
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 MoreLet's discuss how we can help with your next digital project.
Get Started