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 updatenetwork-only: Always fetch, update cachecache-only: Only return cache, never fetchno-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-firstpolicy 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:
- Identify the screens with the most complex data requirements
- Prototype those screens with GraphQL and measure the improvement in request count and data transfer
- Start with a single screen or feature, not a full migration
- Use Apollo Client for its normalised caching and developer tools
- 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.