GraphQL for Mobile Apps: When REST Isn’t Enough

REST has served mobile developers well for over a decade. Its simplicity, universality, and mature tooling make it the default choice for mobile API design. But REST has limitations that become painful as mobile apps grow in complexity.

GraphQL, developed internally at Facebook in 2012 and open-sourced in 2015, addresses these pain points directly. It is not a REST replacement; it is an alternative that excels in specific scenarios. This guide covers when GraphQL makes sense for mobile apps, how to implement it, and the trade-offs to consider.

The Problems with REST for Mobile

REST works well until it does not. Here are the scenarios where REST creates friction for mobile developers:

Over-Fetching

A REST endpoint returns a fixed data structure. If your user profile endpoint returns 30 fields but the mobile list view only needs 3, you transfer 10 times more data than necessary. On mobile networks, this wasted bandwidth matters.

GET /api/users/123
// Returns: name, email, avatar, bio, location, website, joinDate,
// followers, following, posts, likes, badges, settings, preferences...
// You only needed: name, avatar, bio

Under-Fetching

The opposite problem. To display a project detail screen, you might need the project, its tasks, and each task’s assigned user. With REST, that is three separate requests:

GET /api/projects/123
GET /api/projects/123/tasks
GET /api/users/456  (for each assigned user)

Each request incurs network latency. On a 200ms mobile connection, three sequential requests mean 600ms minimum before the screen is fully loaded.

The N+1 Problem

Displaying a list of projects with their owners requires one request for the project list and then one request per owner: N+1 requests total. This scales poorly and creates waterfalls of network requests.

Versioning Complexity

REST APIs version via URLs (/api/v1/, /api/v2/) or headers. Mobile apps cannot be force-updated, so you maintain multiple API versions simultaneously. With diverse client versions in the wild, this becomes a maintenance burden.

How GraphQL Solves The

se Problems

GraphQL uses a single endpoint with a query language that lets the client specify exactly what data it needs.

Queries: Get Exactly What You Need

query ProjectDetail {
  project(id: "123") {
    name
    description
    status
    tasks {
      id
      title
      isCompleted
      assignee {
        name
        avatar
      }
    }
  }
}

One request. Exactly the data needed. No over-fetching, no under-fetching.

Mutations: Modify Data

mutation CreateTask {
  createTask(input: {
    projectId: "123"
    title: "Design onboarding flow"
    priority: HIGH
  }) {
    id
    title
    createdAt
  }
}

Mutations explicitly describe what is being changed and what data to return in the response.

Subscriptions: Real-Time Updates

subscription TaskUpdated {
  taskUpdated(projectId: "123") {
    id
    title
    isCompleted
    assignee {
      name
    }
  }
}

Subscriptions provide real-time data push over WebSockets, integrated into the same query language.

Implementing GraphQL in

Mobile Apps

React Native with Apollo Client

Apollo Client is the most popular GraphQL client for React and React Native:

npm install @apollo/client graphql
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.yourapp.com/graphql',
  cache: new InMemoryCache(),
  headers: {
    authorization: `Bearer ${authToken}`,
  },
});

const App = () => (
  <ApolloProvider client={client}>
    <NavigationContainer>
      <RootNavigator />
    </NavigationContainer>
  </ApolloProvider>
);

Querying Data

import { useQuery, gql } from '@apollo/client';

const GET_PROJECTS = gql`
  query GetProjects($limit: Int, $offset: Int) {
    projects(limit: $limit, offset: $offset) {
      id
      name
      description
      taskCount
      completionPercentage
      owner {
        name
        avatar
      }
    }
  }
`;

const ProjectListScreen = () => {
  const { loading, error, data, refetch } = useQuery(GET_PROJECTS, {
    variables: { limit: 20, offset: 0 },
  });

  if (loading) return <SkeletonList />;
  if (error) return <ErrorState message={error.message} onRetry={refetch} />;

  return (
    <FlatList
      data={data.projects}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <ProjectCard project={item} />}
      onRefresh={refetch}
      refreshing={loading}
    />
  );
};

Mutations

import { useMutation, gql } from '@apollo/client';

const CREATE_TASK = gql`
  mutation CreateTask($input: CreateTaskInput!) {
    createTask(input: $input) {
      id
      title
      isCompleted
      createdAt
    }
  }
`;

const AddTaskScreen = ({ projectId }) => {
  const [title, setTitle] = useState('');
  const [createTask, { loading }] = useMutation(CREATE_TASK, {
    update(cache, { data: { createTask: newTask } }) {
      // Update the cache to include the new task
      cache.modify({
        id: cache.identify({ __typename: 'Project', id: projectId }),
        fields: {
          tasks(existingTasks = []) {
            const newTaskRef = cache.writeFragment({
              data: newTask,
              fragment: gql`
                fragment NewTask on Task {
                  id
                  title
                  isCompleted
                  createdAt
                }
              `,
            });
            return [...existingTasks, newTaskRef];
          },
        },
      });
    },
  });

  const handleSubmit = () => {
    createTask({
      variables: {
        input: { projectId, title, priority: 'MEDIUM' },
      },
    });
  };

  return (
    <View>
      <TextInput value={title} onChangeText={setTitle} placeholder="Task title" />
      <Button title="Create Task" onPress={handleSubmit} disabled={loading} />
    </View>
  );
};

iOS with Apollo iOS

For native iOS apps, Apollo provides a Swift client:

import Apollo

let client = ApolloClient(url: URL(string: "https://api.yourapp.com/graphql")!)

// Generate type-safe query classes from your schema
// Using Apollo's code generation tool
client.fetch(query: GetProjectsQuery(limit: 20, offset: 0)) { result in
    switch result {
    case .success(let graphQLResult):
        if let projects = graphQLResult.data?.projects {
            self.updateUI(with: projects)
        }
    case .failure(let error):
        self.showError(error)
    }
}

Apollo iOS generates type-safe Swift code from your GraphQL schema and queries, catching errors at compile time.

Android with Apollo Kotlin

val apolloClient = ApolloClient.Builder()
    .serverUrl("https://api.yourapp.com/graphql")
    .build()

// Generated type-safe query
val response = apolloClient.query(GetProjectsQuery(limit = 20, offset = 0)).execute()

response.data?.projects?.forEach { project ->
    println("Project: ${project.n

ame}")
}

Caching in GraphQL

Effective caching is critical for mobile performance. Apollo Client provides a normalised cache that is more sophisticated than simple response caching.

Normalised Cache

Apollo breaks down query responses into individual objects and stores them by type and ID. If two queries return the same project (identified by __typename and id), they share the same cache entry.

const client = new ApolloClient({
  uri: 'https://api.yourapp.com/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      Project: {
        fields: {
          tasks: {
            merge(existing = [], incoming) {
              return [...incoming]; // Replace tasks on refresh
            },
          },
        },
      },
    },
  }),
});

Cache-First Strategy

For mobile apps, a cache-first strategy provides the best user experience:

const { data } = useQuery(GET_PROJECTS, {
  fetchPolicy: 'cache-and-network',
  // Returns cached data immediately, then updates with fresh data
});

Available fetch policies:

  • cache-first: Return cache if available, otherwise fetch (default)
  • cache-and-network: Return cache immediately, then fetch and update
  • network-only: Always fetch, update cache
  • cache-only: Only return cache, never fetch
  • no-cache: Fetch without caching

For most mobile screens, cache-and-network provides the best balance of speed and freshness.

When to Use GraphQL vs REST

Choose GraphQL When:

  • Your app has complex, nested data relationships
  • Different screens need different subsets of the same data
  • You want to reduce the number of network requests
  • Real-time subscriptions are important
  • Multiple client types (iOS, Android, web) consume the same API
  • Your data model changes frequently

Stick with REST When:

  • Your API is simple with flat resources
  • You have a well-defined, stable data model
  • File uploads are a primary use case (GraphQL handles uploads but REST is simpler)
  • Your team has limited GraphQL experience and needs to ship quickly
  • Caching requirements are simple (HTTP caching works well for REST)

Hybrid Approach

You do not have to choose exclusively. Many production apps use both:

  • GraphQL for complex data fetching (project details, dashboards)
  • REST for simple operations (file uploads, webhooks, third-party integrations)

GraphQL Challenges for Mobile

Offline Support

GraphQL does not have built-in offline support like Firestore. You need to implement it:

  • Use Apollo’s cache-first policy for offline reads
  • Queue mutations when offline and replay when online
  • Handle conflicts when offline mutations conflict with server changes

Error Handling

GraphQL always returns HTTP 200 (even with errors). Errors are in the response body:

const { data, error } = useQuery(GET_PROJECTS);

if (error) {
  // Check for specific error types
  if (error.graphQLErrors.some(e => e.extensions?.code === 'UNAUTHENTICATED')) {
    navigation.navigate('Login');
  } else {
    showErrorMessage(error.message);
  }
}

Query Complexity

Clients can request deeply nested data, potentially creating expensive server operations. Implement query depth limiting and complexity analysis on your server:

// Server-side: Limit query depth
const depthLimit = require('graphql-depth-limit');

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(5)],
});

Building a GraphQL Backend

If you are building your own GraphQL server, popular options include:

  • Apollo Server (Node.js): The most popular JavaScript GraphQL server
  • Hasura: Instant GraphQL API on PostgreSQL (great for rapid development)
  • AWS AppSync: Managed GraphQL service with offline support
  • PostGraphile: Auto-generated GraphQL API from PostgreSQL schema

For Australian startups, Hasura deserves special attention. It generates a production-ready GraphQL API from your PostgreSQL database in minutes, with real-time subscriptions, authentication, and authorisation built in. It is an excellent choice for MVPs that need GraphQL quickly.

Getting Started

If you are considering GraphQL for your mobile app:

  1. Identify the screens with the most complex data requirements
  2. Prototype those screens with GraphQL and measure the improvement in request count and data transfer
  3. Start with a single screen or feature, not a full migration
  4. Use Apollo Client for its normalised caching and developer tools
  5. Implement error handling and loading states comprehensively

GraphQL is not a silver bullet, but for the right use cases, it significantly improves both the developer experience and the end-user experience. At eawesome, we evaluate GraphQL versus REST on a project-by-project basis and choose the approach that best serves our clients’ apps.