Loading learning content...
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 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:
Traditional Web Applications vs. Single-Page Applications:
Web architectures have evolved significantly:
Traditional Server-Rendered (Multi-Page Applications):
Single-Page Applications (SPAs):
| 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 Origin header and expects Access-Control-Allow-Origin in 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 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:
Mobile Network Challenges:
Mobile networks are fundamentally less reliable than wired connections. Designing for mobile means assuming:
| 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:
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.
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.
| 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.
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:
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).
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 bodiesStrategy 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| 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.
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:
In December, your server receives requests from:
You cannot simply deprecate API v1—you'd break 40% of your users.
/api/v1/users, /api/v2/users. Clear, cache-friendly, but pollutes URLs and complicates routing./api/users?version=1. Flexible but less discoverable and may interfere with caching.Accept: application/vnd.myapp.v1+json. Clean URLs but requires custom header handling.Backward-Compatible Changes (Non-Breaking):
These changes can be made without breaking existing clients:
Breaking Changes (Require New Version):
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.
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:
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:
Generated SDKs ensure consistency between API documentation and client libraries, reducing drift.
SDK Maintenance:
SDKs require ongoing investment:
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.
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:
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:
Command-Line Interface (CLI) Clients:
DevOps tools, admin CLIs, and automation scripts require:
--output json for scriptable output| 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.
We've explored the diverse ecosystem of clients that consume services in modern systems. Let's consolidate the key takeaways:
What's Next:
Now that we understand the diversity of clients, we'll turn our attention to the server side of the client-server model. We'll explore application servers, database servers, and cache servers—the layered ecosystem that processes requests and serves responses.
You now have a comprehensive understanding of client diversity: web browsers constrained by CORS and sandboxing, mobile apps facing network unreliability, API consumers requiring programmatic interfaces, and specialized clients with extreme constraints. This knowledge will inform every API and system you design.