Client-Server Model
Clients: Web, Mobile, API Consumers
Read aloud is unavailable in this browser.
The Diverse Ecosystem of Service Consumers
In the previous pages, we defined the client as the entity that initiates requests and consumes services. But 'the client' is not a monolith—it encompasses a diverse ecosystem of platforms, form factors, and consumption patterns, each with unique characteristics that profoundly influence system design.
A web browser rendering HTML is a client. A native iOS app calling REST APIs is a client. A backend microservice querying a database is a client. An IoT sensor publishing telemetry is a client. A CI/CD pipeline fetching artifacts is a client.
Understanding this diversity is essential because the same server must often serve vastly different clients—with different capabilities, constraints, connectivity, and user expectations. The design decisions you make for web may break mobile. Optimizations for mobile may not apply to IoT. API design for internal services differs from public SDKs.
This page explores the major client categories—web, mobile, and API consumers—in depth, equipping you to design systems that serve them all effectively.
By the end of this page, you will understand the characteristics and constraints of web clients, native mobile clients, and API consumers. You will learn how each client type influences API design, authentication, data transfer, offline support, and versioning strategies.
Web Clients: Browsers, SPAs, and the Web Platform
Web browsers are the most ubiquitous clients in computing—a universal platform accessible from any device with an internet connection. But the browser environment imposes unique constraints and capabilities that shape how web clients interact with servers.
The Browser Execution Environment:
Browsers execute JavaScript in a sandboxed environment with specific security restrictions. Understanding these constraints is essential for designing APIs that web clients can consume:
- •Same-Origin Policy — Browsers restrict JavaScript from making requests to domains different from the page's origin. Cross-origin requests require explicit CORS (Cross-Origin Resource Sharing) headers from the server.
- •Cookie-Based Sessions — Browsers automatically include cookies with requests to matching domains. This enables session management but requires careful security (HttpOnly, Secure, SameSite attributes).
- •Limited Persistent Storage — LocalStorage, IndexedDB, and the Cache API provide client-side storage, but with significant size limits (~5-10MB for LocalStorage) and potential clearing by users or browsers.
- •No Direct TCP/UDP Access — Browsers cannot open raw socket connections. All communication uses HTTP, WebSocket, or Web RTC (for peer-to-peer).
- •User-Controlled Environment — Users can disable JavaScript, block cookies, use ad blockers, or inspect all network traffic. Nothing in the browser can be truly 'secured' from the user.
- •Automatic Credential Management — Browsers handle TLS certificate validation, authentication prompt UI, and credential storage—abstracting complexity but limiting control.
Traditional Web Applications vs. Single-Page Applications:
Web architectures have evolved significantly:
Traditional Server-Rendered (Multi-Page Applications):
- Server generates complete HTML pages for each request
- Browser makes full-page navigations between views
- Minimal JavaScript; logic lives on the server
- Better for SEO, initial load performance, and simpler interactivity
Single-Page Applications (SPAs):
- Initial HTML load includes JavaScript application
- JavaScript handles routing, rendering, and state management
- API calls fetch data; UI updates without full page reloads
- Richer interactivity but more complex development model
| Aspect | Traditional MPA | Single-Page Application | Hybrid/SSR |
|---|---|---|---|
| Rendering | Server-side (PHP, Rails, etc.) | Client-side (React, Vue, Angular) | Both (Next.js, Nuxt, Remix) |
| Navigation | Full page reload | Client-side routing | Flexible |
| Initial Load | Faster (less JS) | Slower (JS bundle) | Optimized (code splitting) |
| SEO | Native support | Requires workarounds | Native support |
| Interactivity | Limited | Rich | Rich |
| API Dependency | Low (HTML from server) | High (data via APIs) | Moderate |
| Complexity | Lower | Higher | Higher |
CORS: The Cross-Origin Gate:
CORS is the security mechanism that governs cross-origin requests. When your SPA at app.example.com calls an API at api.example.com, the browser enforces CORS:
-
For 'simple' requests (GET with standard headers), the browser adds an
Originheader and expectsAccess-Control-Allow-Originin the response. -
For 'preflighted' requests (POST with JSON, custom headers), the browser first sends an OPTIONS request to check if the actual request is permitted.
Servers must explicitly whitelist origins, methods, and headers. Misconfigured CORS is a common source of API integration failures.
1234567891011121314
# CORS Preflight Request (OPTIONS)OPTIONS /api/orders HTTP/1.1Host: api.example.comOrigin: https://app.example.comAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: Content-Type, Authorization # Server Response (allowing the request)HTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Methods: GET, POST, PUT, DELETEAccess-Control-Allow-Headers: Content-Type, AuthorizationAccess-Control-Max-Age: 86400Access-Control-Allow-Credentials: trueCORS protects users from malicious sites making requests on their behalf, not servers from unauthorized access. Your API must still authenticate and authorize every request. CORS is browser-enforced; non-browser clients (curl, mobile apps, servers) ignore it entirely.
Mobile Clients: Native Apps, Hybrid, and PWAs
Mobile clients represent a fundamentally different environment than browsers: they have more device access, face network unreliability, and operate under strict platform guidelines. Understanding mobile's unique constraints shapes API design, authentication, and user experience.
Native Mobile Applications (iOS/Android):
Native apps are installed from app stores and written in platform-specific languages (Swift/Kotlin) or cross-platform frameworks (React Native, Flutter). They have characteristics distinct from web clients:
- •No Same-Origin Restrictions — Unlike browsers, native apps can call any server without CORS. This simplifies API access but shifts security burden to authentication.
- •Secure Storage Available — iOS Keychain and Android Keystore provide hardware-backed secure storage for tokens and credentials—more secure than browser storage.
- •Persistent Connections — Apps can maintain long-lived connections (WebSocket, gRPC) for real-time updates, background sync, and push notifications.
- •Background Execution (Limited) — iOS and Android restrict background processing to conserve battery, requiring careful design for background sync and notifications.
- •Device Capabilities — Access to camera, GPS, accelerometer, biometrics, and other sensors enables rich functionality unavailable to web.
- •Update Cycle Constraints — App store review processes create deployment delays. You cannot instantly deploy fixes like web—backward compatibility is critical.
- •Version Fragmentation — Users may run old app versions for months or years. APIs must support multiple client versions simultaneously.
Mobile Network Challenges:
Mobile networks are fundamentally less reliable than wired connections. Designing for mobile means assuming:
- Variable Latency: 100ms on 5G, 2000ms on edge networks, spikes during handoffs
- Intermittent Connectivity: Tunnels, elevators, dead zones create disconnection
- Bandwidth Constraints: Data plans, throttling after caps, and slow networks demand efficient payloads
- Battery Impact: Network calls consume significant battery; excessive polling drains devices quickly
| Condition | Characteristics | Design Response |
|---|---|---|
| 5G/LTE | Low latency (20-50ms), high bandwidth | Rich media, real-time features, standard APIs |
| 3G/EDGE | High latency (200-2000ms), low bandwidth | Compressed payloads, fewer requests, offline-first |
| Intermittent | Frequent disconnects, variable quality | Offline storage, retry queues, conflict resolution |
| Metered | Data caps, pay-per-byte | Efficient formats (Protobuf), delta sync, user control |
| Roaming | Expensive, potentially blocked | Optional sync, caching, reduced functionality |
Offline-First Design:
Mobile apps increasingly adopt offline-first architecture—assuming unreliable connectivity and designing the app to function without a network:
- Local Database: SQLite, Realm, or similar stores data locally
- Optimistic UI: Show changes immediately; sync in background
- Sync Queue: Queue changes when offline; sync when connected
- Conflict Resolution: When local and server state diverge, resolve using last-write-wins, merge, or user intervention
- Background Sync: Sync data during background execution windows
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// Offline-First Sync Pattern interface SyncOperation { id: string; type: 'create' | 'update' | 'delete'; resource: string; data: any; timestamp: number; retryCount: number;} class OfflineSyncManager { private db: LocalDatabase; private syncQueue: SyncOperation[] = []; // Save change locally first (instant UI update) async saveLocally(resource: string, data: any): Promise<void> { const operation: SyncOperation = { id: generateUUID(), type: 'update', resource, data, timestamp: Date.now(), retryCount: 0 }; // Update local database immediately await this.db.save(resource, data); // Queue for server sync this.syncQueue.push(operation); await this.persistQueue(); // Trigger sync if online if (navigator.onLine) { this.attemptSync(); } } // Sync queued operations to server async attemptSync(): Promise<void> { const operations = [...this.syncQueue]; for (const op of operations) { try { await this.syncToServer(op); this.syncQueue = this.syncQueue.filter(o => o.id !== op.id); } catch (error) { if (this.isRetryable(error)) { op.retryCount++; // Exponential backoff await this.scheduleRetry(op); } else { this.handleConflict(op, error); } } } await this.persistQueue(); } // Resolve conflicts between local and server state private handleConflict(op: SyncOperation, error: Error): void { // Strategy options: // 1. Last-write-wins: Server version takes precedence // 2. Client-wins: Force local version to server // 3. Merge: Combine changes intelligently // 4. User decision: Present conflict to user }}Constraints breed innovation. Designing for mobile's limitations (offline, battery, bandwidth) often results in APIs that are more efficient, more resilient, and work better for all clients. Treat mobile as a design forcing function, not an afterthought.
API Consumers: Service-to-Service and Programmatic Access
Not all clients have human users. API consumers—including backend services, integration platforms, automation scripts, and third-party developers—are machine clients that interact with your systems programmatically. Their needs differ significantly from human-facing clients.
Types of API Consumers:
Internal Services (Service-to-Service): Microservices, background jobs, and internal tools calling your APIs. They operate in controlled environments with known trust levels.
External Developers (Public APIs): Third-party developers building integrations, mobile apps, or competing services using your API. They operate in uncontrolled environments and may stress or abuse your systems.
Integration Platforms: Zapier, IFTTT, MuleSoft, and similar platforms orchestrating workflows across services. They act as intermediaries, aggregating APIs.
Automation Scripts: CI/CD pipelines, monitoring systems, deployment tools, and operational scripts. They require stable, scriptable interfaces.
- •No UI Rendering — Machine clients don't render HTML or handle user interactions. They need structured data (JSON, Protobuf, XML), not presentation markup.
- •Automated Error Handling — Errors must be programmatically parseable: structured error codes, consistent formats, and actionable messages for automated recovery.
- •High Throughput Potential — Unlike human-driven clients, machine clients can generate requests at machine speed—thousands per second if unthrottled.
- •Strict Reliability Requirements — Machine clients often run unattended; failures may not be noticed immediately. They need robust error response and recovery.
- •Long-Lived Credentials — API keys, service accounts, and OAuth tokens enable authentication without human intervention. Credential security and rotation are critical.
- •Versioning Sensitivity — Breaking API changes immediately break integrations. Machine clients cannot 'adapt' like humans; backward compatibility is essential.
| Consumer Type | Trust Level | Volume | Latency Sensitivity | Key Requirements |
|---|---|---|---|---|
| Internal Services | High | Variable | Usually high | Service mesh, mTLS, tracing |
| Public Developers | Low | Unpredictable | Moderate | Rate limiting, API keys, docs |
| Integration Platforms | Medium | Moderate | Low | Webhooks, stable schemas |
| CI/CD Pipelines | High | Low-moderate | Moderate | Stable CLI, idempotency |
| Mobile Backends | Medium | High | High | GraphQL, efficient payloads |
Authentication for Machine Clients:
Machine clients require authentication mechanisms suited to non-interactive, automated use:
API Keys: Simple, long-lived tokens. Easy to implement but hard to rotate securely. Suitable for low-sensitivity, rate-limited public APIs.
OAuth 2.0 Client Credentials: Service-to-service authentication using client ID and secret. Provides scoped access, token expiration, and centralized management.
Mutual TLS (mTLS): Both client and server present certificates. Strongest authentication for service-to-service communication in zero-trust environments.
JWT/Service Tokens: Short-lived, signed tokens encoding identity and permissions. Enables stateless authentication without per-request database lookups.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// API Authentication Patterns for Machine Clients // Pattern 1: API Key (simple, header-based)const response = await fetch('https://api.example.com/v1/data', { headers: { 'X-API-Key': 'sk_live_1234567890abcdef' }}); // Pattern 2: OAuth 2.0 Client Credentials Flowasync function getAccessToken(): Promise<string> { const tokenResponse = await fetch('https://auth.example.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET, scope: 'read:orders write:orders' }) }); const { access_token } = await tokenResponse.json(); return access_token;} // Use the tokenconst accessToken = await getAccessToken();const response = await fetch('https://api.example.com/v1/orders', { headers: { 'Authorization': `Bearer ${accessToken}` }}); // Pattern 3: Service Account JWT (self-signed)import * as jwt from 'jsonwebtoken'; function createServiceJWT(serviceAccountKey: ServiceAccountKey): string { const now = Math.floor(Date.now() / 1000); return jwt.sign( { iss: serviceAccountKey.client_email, sub: serviceAccountKey.client_email, aud: 'https://api.example.com', iat: now, exp: now + 3600, // 1 hour }, serviceAccountKey.private_key, { algorithm: 'RS256' } );}Machine clients can generate unlimited load. Every public API must implement rate limiting (requests per second/minute/hour), quotas (total requests per billing period), and graceful degradation. Return clear 429 responses with Retry-After headers so clients can back off appropriately.
Designing APIs for Multiple Client Types
Modern systems must serve diverse clients from a single backend. A product might have a web app, iOS and Android apps, a public API, third-party integrations, and internal admin tools—all calling the same services. Designing for this diversity requires intentional strategies.
Strategy 1: Backend for Frontend (BFF)
Instead of one API serving all clients, create specialized backend services for each client type:
- Web BFF: Optimized for SPA consumption, handles CORS, serves aggregated data for page renders
- Mobile BFF: Optimized for bandwidth, returns smaller payloads, supports offline workflows
- Public API: Versioned, rate-limited, thoroughly documented
BFFs aggregate calls to underlying microservices and shape responses for each client's needs.
┌─────────────────────────────────────────────────────────────────────────┐│ CLIENT LAYER │├─────────────────┬──────────────────┬───────────────────┬────────────────┤│ Web App SPA │ iOS App │ Android App │ Third-Party ││ (React) │ (SwiftUI) │ (Kotlin) │ Developers │└────────┬────────┴────────┬─────────┴────────┬──────────┴────────┬───────┘ │ │ │ │ ▼ ▼ ▼ ▼┌─────────────────────────────────────────────────────────────────────────┐│ BACKEND FOR FRONTEND (BFF) LAYER │├─────────────────┬──────────────────┬───────────────────┬────────────────┤│ Web BFF │ Mobile BFF │ │ Public API ││ - GraphQL │ - REST │ │ - REST ││ - CORS │ - Compressed │ │ - Versioned ││ - Sessions │ - Offline │ │ - Documented │└────────┬────────┴────────┬─────────┘ └────────┬───────┘ │ │ │ └────────┬────────┴──────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────────────┐│ CORE SERVICES LAYER │├─────────────────┬──────────────────┬───────────────────┬────────────────┤│ User Service │ Order Service │ Product Service │ Payment Svc │└────────┬────────┴────────┬─────────┴────────┬──────────┴────────┬───────┘ │ │ │ │ └────────┴────────┴──────────────────┴───────────────────┘ │ ▼ [ Databases ]Strategy 2: GraphQL for Flexible Fetching
GraphQL allows clients to request exactly the data they need, eliminating over-fetching (getting more data than needed) and under-fetching (making multiple requests to get all needed data).
- Web dashboards can request rich, nested data in one query
- Mobile apps can request minimal data to conserve bandwidth
- Both use the same GraphQL endpoint with different queries
Strategy 3: Content Negotiation
Use HTTP content negotiation to serve different representations:
Accept: application/json→ Standard JSON responseAccept: application/vnd.api+json→ JSON:API format with relationshipsAccept: application/protobuf→ Binary Protocol Buffer for efficiencyAccept-Encoding: gzip→ Compressed response bodies
Strategy 4: Field Filtering and Sparse Fieldsets
Allow clients to specify which fields they need:
/api/users/123?fields=id,name,email→ Returns only requested fields- Reduces payload size for bandwidth-constrained clients
- Standard in JSON:API and GraphQL
| Strategy | Best For | Trade-offs | Implementation Effort |
|---|---|---|---|
| Backend for Frontend | Diverse client needs, complex aggregation | More code paths, potential duplication | High |
| GraphQL | Flexible fetching, rapid client iteration | Complexity, caching challenges | Medium-High |
| Content Negotiation | Same data, different formats | Limited flexibility | Low-Medium |
| Field Filtering | Over-fetching reduction | Query complexity | Low |
| API Versioning | Evolving APIs, backward compat | Multiple versions to maintain | Medium |
Don't build three BFFs from day one. Start with a single API that serves all clients reasonably well. When client-specific requirements create friction—excessive round trips for mobile, bandwidth inefficiency for low-power devices—introduce targeted BFFs for those clients while keeping others on the general API.
Client Versioning and API Compatibility
Unlike web applications where you can deploy updates instantly, mobile apps and API integrations continue running old versions indefinitely. This creates a multi-version reality: your servers must simultaneously support clients from months or years ago.
The Version Fragmentation Problem:
Consider a mobile app:
- v1.0 released January (using API v1)
- v1.1 released March (still API v1)
- v1.2 released May (requires API v1.1 features)
- v2.0 released September (requires API v2)
In December, your server receives requests from:
- v1.0 users who never updated (3% of traffic)
- v1.1 users (12% of traffic)
- v1.2 users (25% of traffic)
- v2.0 users (60% of traffic)
You cannot simply deprecate API v1—you'd break 40% of your users.
- •URL Path Versioning —
/api/v1/users,/api/v2/users. Clear, cache-friendly, but pollutes URLs and complicates routing. - •Query Parameter Versioning —
/api/users?version=1. Flexible but less discoverable and may interfere with caching. - •Header Versioning —
Accept: application/vnd.myapp.v1+json. Clean URLs but requires custom header handling. - •No Explicit Versioning — Evolve the API additively, never breaking changes. Simplest but requires strict discipline.
Backward-Compatible Changes (Non-Breaking):
These changes can be made without breaking existing clients:
- Adding new endpoints
- Adding optional fields to request bodies
- Adding fields to responses (clients should ignore unknown fields)
- Adding new enum values (if clients use unknown-value handling)
- Making required fields optional
Breaking Changes (Require New Version):
- Removing endpoints or fields
- Renaming endpoints or fields
- Changing field types
- Making optional fields required
- Changing authentication mechanisms
- Altering error response formats
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
// API Response Evolution - Backward Compatible // Original API v1 Response{ "id": "usr_123", "name": "Jane Doe", "email": "jane@example.com"} // API v1.1 - Added optional fields (NON-BREAKING){ "id": "usr_123", "name": "Jane Doe", "email": "jane@example.com", "created_at": "2025-01-15T10:00:00Z", // New field "avatar_url": null // New nullable field} // API v2 - Restructured (BREAKING - requires new version){ "id": "usr_123", "profile": { "display_name": "Jane Doe", // Renamed and nested "email": { "address": "jane@example.com", "verified": true } }, "metadata": { "created_at": "2025-01-15T10:00:00Z" }} // Supporting both: version-aware response handlerfunction formatUserResponse(user: User, apiVersion: string): any { if (apiVersion.startsWith('v1')) { return { id: user.id, name: user.displayName, email: user.email.address, created_at: user.createdAt, // v1.1 field, ignored by v1.0 clients }; } else { return { id: user.id, profile: { display_name: user.displayName, email: { address: user.email.address, verified: user.email.verified } }, metadata: { created_at: user.createdAt } }; }}You cannot support old versions forever. Define a sunset policy: 'We support the last 3 major versions' or 'We deprecate versions after 18 months.' Communicate deprecation clearly through response headers (Deprecation, Sunset), documentation, and direct developer outreach. Give clients time to migrate.
Client SDKs and Developer Experience
For public APIs and complex internal services, HTTP endpoints alone may not suffice. Client SDKs (Software Development Kits) wrap your API in language-native libraries, improving developer productivity and reducing integration errors.
Why Provide SDKs?
SDKs abstract the complexity of API consumption:
- Type Safety: Static types catch errors at compile time, not runtime
- Automatic Authentication: SDK handles token refresh, retry, and credential storage
- Serialization/Deserialization: Objects instead of raw JSON
- Retry Logic: Built-in handling for transient failures
- Pagination: Iterators that transparently fetch additional pages
- Idiomatic Code: Patterns familiar to the target language community
- •Construct HTTP requests manually
- •Parse JSON responses without types
- •Implement authentication flow
- •Handle pagination manually
- •Implement retry with backoff
- •No IDE autocompletion
- •Import and call methods
- •Receive typed objects
- •Auth handled by client constructor
- •Iterate over pageable results
- •Retries built in
- •Full IDE support and autocompletion
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// WITHOUT SDK: Manual HTTP Consumptionasync function getOrders(): Promise<Order[]> { const token = await getAccessToken(); let allOrders: Order[] = []; let nextPageUrl: string | null = 'https://api.example.com/v1/orders?limit=100'; while (nextPageUrl) { const response = await fetch(nextPageUrl, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' } }); if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') || '60'); await sleep(retryAfter * 1000); continue; } if (!response.ok) { throw new Error(`API error: ${response.status}`); } const data = await response.json(); allOrders = allOrders.concat(data.orders); nextPageUrl = data.pagination?.next_url || null; } return allOrders;} // WITH SDK: Idiomatic TypeScriptimport { ExampleClient } from '@example/sdk'; const client = new ExampleClient({ apiKey: process.env.API_KEY, // Auth, retries, pagination handled internally}); async function getOrders(): Promise<Order[]> { // Method is typed; returns Order[] // Pagination is automatic; iterate as needed const orders: Order[] = []; for await (const order of client.orders.list()) { orders.push(order); } return orders;}SDK Generation:
Modern SDKs are often generated from API specifications:
- OpenAPI/Swagger → Generate SDKs in any language using tools like OpenAPI Generator, Swagger Codegen
- GraphQL Schema → Generate typed clients using codegen tools (graphql-code-generator)
- Protocol Buffers → Generate clients using protoc with language plugins
Generated SDKs ensure consistency between API documentation and client libraries, reducing drift.
SDK Maintenance:
SDKs require ongoing investment:
- Version SDKs to match API versions
- Release updates when API changes
- Fix language-specific bugs and compatibility issues
- Document usage patterns and provide examples
- Support major versions of target languages/platforms
Even if you don't generate SDKs, define your API in OpenAPI. This provides machine-readable documentation, enables code generation when needed, powers interactive documentation (Swagger UI), and validates request/response schemas. It's the foundation of good API developer experience.
Special Client Categories: IoT, Edge, and Embedded
Beyond web, mobile, and API consumers, specialized client categories present unique constraints. Understanding these edge cases informs design decisions for the broadest possible client support.
IoT and Embedded Devices:
Internet of Things devices (sensors, controllers, monitors) and embedded systems (industrial equipment, wearables, automotive) operate under severe constraints:
- •Limited CPU/Memory — Cannot parse complex JSON or run heavy cryptographic operations. May need binary protocols (CBOR, MessagePack, Protobuf) and simpler auth.
- •Battery Constraints — Must minimize radio usage. Prefer infrequent bulk uploads over frequent small requests. May sleep for hours between transmissions.
- •Connectivity — May use cellular (expensive, unreliable), LoRaWAN (low bandwidth), or satellite (very expensive, high latency). HTTP may be too heavy; MQTT or CoAP preferred.
- •Security Limitations — May lack hardware security modules. Certificate rotation and TLS may be challenging. Need secure provisioning workflows.
- •Long Deployment Cycles — Firmware updates are difficult or impossible. Devices deployed today may run the same code for 10 years. Stability is paramount.
- •Massive Scale — IoT deployments may involve millions of devices, each sending telemetry. Backend must handle high cardinality, bursty traffic.
Edge Clients:
Edge computing pushes computation closer to clients—CDN edge nodes, local gateways, or edge servers. These 'clients' (from the perspective of origin servers) have unique characteristics:
- Act as Both Client and Server: Receive requests from end users, call origin servers for uncached content
- Caching Semantics: Must understand HTTP cache semantics to serve cached content correctly
- Limited Storage: Edge caches have finite capacity; must evict intelligently
- Distributed State: Cannot rely on shared memory; use eventual consistency
Command-Line Interface (CLI) Clients:
DevOps tools, admin CLIs, and automation scripts require:
- Stable Command Structure: Breaking CLI arguments breaks scripts
- Machine-Parseable Output: Support
--output jsonfor scriptable output - Exit Codes: Meaningful exit codes for scripted error handling
- Stdin/Stdout Support: Enable piping data for Unix-style composition
| Client Type | Typical Constraints | Preferred Protocols | Key Design Considerations |
|---|---|---|---|
| IoT Sensors | Battery, connectivity, CPU | MQTT, CoAP, binary formats | Efficiency, batching, offline queuing |
| Wearables | Battery, small display | Bluetooth LE, compact HTTP | Minimal payloads, background sync |
| Edge Nodes | Cache capacity, latency | HTTP, internal RPC | Cache headers, origin failover |
| Embedded Systems | Memory, update difficulty | Binary protocols, UDP | Stability, long-term support |
| CLI Tools | Scriptability, automation | HTTP, native protocols | Exit codes, parseable output |
When designing APIs that may serve constrained clients, assume the worst case: slow networks, limited processing, intermittent connectivity. Features like binary protocol support, efficient compression, and graceful degradation become essential—not optional—when serving IoT and embedded devices.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.