REST API Design Patterns for Mobile Applications

Designing APIs for mobile apps requires a different mindset than building APIs for web applications. Mobile clients operate under unique constraints: unreliable network connections, limited bandwidth, battery consumption concerns, and diverse device capabilities. An API that works perfectly for a web frontend may perform poorly when consumed by a mobile app.

This guide covers the design patterns and practices that produce mobile-friendly REST APIs. Whether you are building your own backend or working with a backend team, understanding these patterns will help you deliver a better mobile experience.

Resource-Oriented URLs

REST APIs are organised around resources. Each resource has a URL, and HTTP methods define the operations available on that resource.

URL Structure

Follow these conventions for clean, predictable URLs:

GET    /api/v1/projects          # List projects
POST   /api/v1/projects          # Create a project
GET    /api/v1/projects/123      # Get a specific project
PUT    /api/v1/projects/123      # Update a project
DELETE /api/v1/projects/123      # Delete a project
GET    /api/v1/projects/123/tasks  # List tasks for a project

Use nouns, not verbs. The HTTP method provides the verb. /api/v1/projects with a GET is “list projects.” You do not need /api/v1/getProjects.

Use plural nouns. /projects is clearer than /project. It represents a collection.

Nest resources logically. /projects/123/tasks clearly communicates the relationship. But avoid nesting deeper than two levels. If you need /projects/123/tasks/456/comments, consider flattening to /comments?taskId=456.

API Versioning

Always version your API from day one. Mobile apps cannot be force-updated like web apps. Users running older app versions will still hit your API, and you need to maintain backwards compatibility.

/api/v1/projects
/api/v2/projects

URL-based versioning is the most common and most explicit approach. Header-based versioning (using Accept headers) is more RESTful in theory but harder to debug and test.

Mobile-Optimised Responses

Sparse Fieldsets

Mobile apps rarely need every field on a resource. Transferring unnecessary data wastes bandwidth and battery. Implement sparse fieldsets to let clients request only the fields they need:

GET /api/v1/projects?fields=id,title,status,updatedAt

The response includes only the requested fields:

{
  "data": [
    {
      "id": "123",
      "title": "Mobile App Redesign",
      "status": "active",
      "updatedAt": "2021-03-10T08:30:00Z"
    }
  ]
}

This pattern significantly reduces payload size for list views where only summary data is needed.

Compound Documents (Includes)

The opposite of sparse fieldsets: sometimes the mobile client needs related resources in a single request. Network round trips are expensive on mobile, so reducing the number of requests is critical.

GET /api/v1/projects/123?include=tasks,members
{
  "data": {
    "id": "123",
    "title": "Mobile App Redesign",
    "tasks": [
      {"id": "1", "title": "Design mockups", "status": "done"},
      {"id": "2", "title": "Implement UI", "status": "in_progress"}
    ],
    "members": [
      {"id": "10", "name": "Sarah", "role": "designer"},
      {"id": "11", "name": "James", "role": "developer"}
    ]
  }
}

Without compound documents, the client would need three separate requests: one for the project, one for tasks, and one for members. On a slow mobile connection, that is the difference between a fast load and a frustrating spinner.

Envelope Pattern

Wrap all responses in a consistent envelope:

{
  "data": { },
  "meta": {
    "page": 1,
    "perPage": 20,
    "totalCount": 142,
    "totalPages": 8
  },
  "links": {
    "self": "/api/v1/projects?page=1",
    "next": "/api/v1/projects?page=2",
    "last": "/api/v1/projects?page=8"
  }
}

The envelope provides metadata and pagination links without polluting the resource data. Clients can parse responses predictably regardless of the resource type.

Pagination

Every list endpoint must be paginated. Returning unbounded lists is a recipe for performance problems on mobile devices.

Cursor-Based Pagination

For mobile apps, cursor-based pagination is superior to offset-based pagination. It handles real-time data (new items being added) gracefully and performs better on large datasets.

GET /api/v1/projects/123/tasks?limit=20&after=cursor_abc123
{
  "data": [ ],
  "meta": {
    "hasNextPage": true,
    "endCursor": "cursor_def456"
  }
}

The cursor is typically an opaque string encoding the position in the dataset (often a base64-encoded ID or timestamp).

Offset-Based Pagination

Simpler to implement but problematic with real-time data:

GET /api/v1/projects?page=2&perPage=20

If items are added or removed between page requests, users may see duplicates or miss items. For less dynamic data, offset pagination is acceptable.

Infinite Scroll Support

Mobile apps commonly use infinite scroll patterns. Your API should support this efficiently:

  1. Return a consistent page size (20 to 50 items is typical)
  2. Include a clear signal for “no more data” (hasNextPage: false)
  3. Use cursor-based pagination to avoid data shifting issues
  4. Keep response times under 200 milliseconds for smooth scrolling

Caching

Effect

ive caching reduces network requests, improves performance, and saves battery life.

HTTP Cache Headers

Use standard HTTP cache headers to enable client-side and CDN caching:

# Response headers
Cache-Control: private, max-age=300
ETag: "abc123"
Last-Modified: Wed, 10 Mar 2021 08:30:00 GMT

Cache-Control tells the client how long the response is valid. private prevents CDN caching of user-specific data.

ETag enables conditional requests. The client can send If-None-Match: "abc123" with subsequent requests. If the data has not changed, the server returns 304 Not Modified with no body, saving bandwidth.

Last-Modified enables If-Modified-Since conditional requests, similar to ETags.

Cache Strategy by Resource Type

Different resources deserve different cache strategies:

Resource TypeCache-ControlStrategy
User profilemax-age=60Short cache, changes infrequently
Project listmax-age=30Short cache, may change often
Static configmax-age=86400Long cache, rarely changes
Notificationsno-cacheAlways fresh
Search resultsmax-age=300Medium cache

Conditional Requests Implementation

// Express.js middleware for ETag support
const express = require('express');
const crypto = require('crypto');

app.get('/api/v1/projects/:id', async (req, res) => {
  const project = await Project.findById(req.params.id);

  const etag = crypto
    .createHash('md5')
    .update(JSON.stringify(project))
    .digest('hex');

  res.set('ETag', `"${etag}"`);
  res.set('Cache-Control', 'private, max-age=60');

  if (req.headers['if-none-match'] === `"${etag}"`) {
    return res.status(304).end();
  }

  res.json({ data: project });
});

Error Handling

Consistent, informative error responses help mobile developers build robust apps.

Error Response Format

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid fields",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address"
      },
      {
        "field": "name",
        "message": "Must be between 2 and 100 characters"
      }
    ]
  }
}

HTTP Status Codes

Use status codes correctly. Mobile clients rely on them for error handling:

  • 200 OK: Successful GET, PUT, PATCH
  • 201 Created: Successful POST that creates a resource
  • 204 No Content: Successful DELETE
  • 400 Bad Request: Invalid request data (validation errors)
  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: Authenticated but insufficient permissions
  • 404 Not Found: Resource does not exist
  • 409 Conflict: State conflict (e.g., duplicate email)
  • 422 Unprocessable Entity: Valid JSON but semantically invalid
  • 429 Too Many Requests: Rate limited
  • 500 Internal Server Error: Server-side failure

Rate Limiting

Protect your API with rate limiting and communicate limits clearly:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1615363200
Retry-After: 60

Mobile clients should respect Retry-After headers and implement exponential backoff for 429 responses.

Authentication

Token-Based Authentication

JWT (JSON Web Tokens) are the standard for mobile API authentication:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Refresh Token Flow

Access tokens should be short-lived (15 to 60 minutes). Use refresh tokens for seamless re-authentication:

  1. User logs in, receives access token and refresh token
  2. Access token is sent with every request
  3. When the access token expires (401 response), the client uses the refresh token to get a new access token
  4. If the refresh token is also expired, the user must log in again
// Mobile client refresh token logic
const apiCall = async (url, options) => {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${accessToken}`,
    },
  });

  if (response.status === 401) {
    const refreshResponse = await fetch('/api/v1/auth/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    });

    if (refreshResponse.ok) {
      const { accessToken: newToken } = await refreshResponse.json();
      accessToken = newToken;

      // Retry original request with new token
      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${newToken}`,
        },
      });
    }
  }

  return response;
};

Store refresh tokens securely: Keychain on iOS, Encrypted SharedPreferences on Android.

Handling Slow and Offline Connections

Request Timeouts

Mobile connections are unreliable. Set reasonable timeouts:

  • Connection timeout: 10 seconds
  • Read timeout: 30 seconds
  • Write timeout: 30 seconds

Retry Logic

Implement retry logic with exponential backoff for idempotent requests (GET, PUT, DELETE):

const fetchWithRetry = async (url, options, maxRetries = 3) => {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      if (response.status >= 500 && attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      return response;
    } catch (error) {
      if (attempt === maxRetries) throw error;
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
};

Never retry non-idempotent POST requests automatically. This can cause duplicate operations.

Compression

Enable gzip or Brotli compression on your API responses. This typically reduces payload size by 60 to 80 percent, which is significant on mobile connections:

// Express.js compression
const compression = require('compression');
app.use(compression());

Documentation

Good API documentation is essential for productive mobile development. Use OpenAPI (Swagger) to document your API:

openapi: 3.0.0
info:
  title: Project Management API
  version: 1.0.0
paths:
  /api/v1/projects:
    get:
      summary: List projects
      parameters:
        - name: fields
          in: query
          description: Comma-separated list of fields to include
          schema:
            type: string
        - name: limit
          in: query
          description: Number of items per page
          schema:
            type: integer
            default: 20

Generate interactive documentation with tools like Swagger UI or Redoc. This lets mobile developers explore and test endpoints during development.

Summary

Building mobile-friendly REST APIs requires attention to the unique constraints of mobile platforms. The key principles are:

  1. Minimise network requests with compound documents and batch operations
  2. Minimise payload size with sparse fieldsets and compression
  3. Enable caching with proper HTTP headers
  4. Handle errors consistently with structured error responses
  5. Paginate everything with cursor-based pagination
  6. Secure properly with short-lived tokens and secure storage
  7. Design for offline with timeouts, retries, and graceful degradation

These patterns are not theoretical. They directly impact the user experience of your mobile app. At eawesome, we design and build APIs specifically optimised for mobile consumption, and we have seen these patterns make a measurable difference in app performance and user satisfaction.