Easily restore your project to a previous version with our new Instant One-click Backup Recovery

GraphQL pain points and how to overcome them

This article addresses the primary pain points of using GraphQL, offering clear and actionable solutions to navigate and overcome these challenges, enhancing user experiences, and improving development workflow.
Joel Olawanle

Written by Joel

Apr 21, 2024
GraphQL pain points and how to overcome them

GraphQL stands out by allowing developers to retrieve exactly what they need from a single endpoint through a single query, making it a compelling choice over traditional REST APIs for everything from web to mobile applications.

This article addresses the primary pain points of using GraphQL, offering clear and actionable solutions to navigate and overcome these challenges, enhancing user experiences, and improving development workflow.

#GraphQL pain points and solutions

Despite its numerous benefits, using GraphQL in production comes with several challenges. This is reflected in the 2024 GraphQL survey. These issues range from performance to security concerns, and they require thoughtful solutions to ensure that these drawbacks do not overshadow the benefits of GraphQL in enabling versatile and efficient APIs.

Let’s explore them.

Query complexity

As the scope of a project grows, GraphQL queries can become increasingly complex, affecting execution time and resource consumption.

In the context of GraphQL, query complexity refers to the assessment of the computational resources that a server would need to fulfill a request. The complexity of a query increases with the number of fields and the depth of the query. Assessing query complexity is important because, if high, it can lead to performance issues.

Here are reasons that can cause complexity and how to overcome them:

1. Deeply nested queries: GraphQL allows clients to use a single request for nested data in a single query. This can lead to deeply nested queries, which may result in poor performance, extensive database joins, or complex data fetching logic — increasing the execution time.

For example, a complex query might request books, their authors, the author's other books, and reviews for those books; this creates a deeply nested structure:

query {
books {
title
author {
name
books {
title
reviews {
content
rating
user {
name
}
}
}
}
}
}

To reduce the complexity caused by deep nesting, it's advisable to break down the query into smaller, more manageable parts.

For example, the first query can fetch a list of books and their authors' names. At this stage, it avoids delving into the authors' other works or the reviews of the books.

query {
books {
title
author {
name
}
}
}

After obtaining the authors from the first query, you can make a separate query to retrieve other books by a specific author. This query can be executed as needed for individual authors, reducing the immediate load.

query {
author(id: "authorId") {
books {
title
}
}
}

Similarly, reviews for a book can be fetched in a standalone query. This allows the application to request reviews only when necessary, which minimizes complexity and resource consumption.

query {
book(id: "bookId") {
reviews {
content
rating
user {
name
}
}
}
}

2. Over-fetching of fields: One of the primary advantages of GraphQL is its ability to mitigate over-fetching, where clients receive more data than they need. Despite this, it's still possible to encounter over-fetching if the queries are not carefully constructed.

Over-fetching can lead to increased processing time and slower response rates, as unnecessary data is processed and transmitted over the network.

Consider a query that requests all available information about a book, even when only a subset of the data is required for a particular operation:

query {
books {
id
title
author {
name
biography
}
publishedDate
genres
summary
reviews {
content
rating
user {
name
email
}
}
}
}

Tailoring the query to request only the fields needed to avoid over-fetching is crucial. For instance, if the goal is to display a list of book titles along with their authors' names, then the query can be simplified as follows:

query {
books {
title
author {
name
}
}
}

3. Pagination for large lists: Queries that return large lists of data can be slow to execute, especially if each item in the list requires additional database lookups to resolve related fields. To overcome this problem, you can implement pagination using first, last, and after arguments.

Suppose you want to fetch a list of the first ten books with pagination. The query can look like this:

query {
books(first: 10) {
title
author {
name
}
}
}

Learning curve

If you’re new to GraphQL, the learning curve to becoming proficient can be steep. A practical approach to flattening this curve is dedicating time to understanding GraphQL concepts and best practices.

Start with basic queries and mutations, then focus on understanding how schemas and resolvers work. Incrementally advance to more complex topics such as query optimization, security, and subscription management to build comprehensive knowledge.

For those seeking to deepen their understanding and expertise in GraphQL, you can explore Hygraph’s detailed GraphQL Academy, which breaks down each GraphQL topic into chapters. Additionally, our getting started flow includes a document on queries that provides initial practice opportunities.

Exposing sensitive data

A single GraphQL endpoint can inadvertently expose sensitive data due to its highly flexible query structure, allowing clients to request exactly what they need. Without stringent authentication and authorization checks, an unauthorized user could potentially query sensitive information they shouldn't have access to.

This risk stems from GraphQL's nature of providing a unified interface to all data, requiring careful implementation of robust authentication and authorization security measures to restrict access based on user roles and permissions.

Authentication verifies user identities, while authorization determines their access levels. In GraphQL, this can be managed by integrating authentication mechanisms (like JWT and OAuth) with GraphQL servers and defining authorization logic within resolver functions.

For example, one approach is to apply middleware to validate tokens before resolver execution. Another method involves using schema directives to control field-level access, ensuring that users fetch only data they can see.

Hygraph allows developers to define specific access controls directly in the schema. This can include setting permissions based on user roles and enabling a highly customizable and secure way to manage access to data within an application.

Backward compatibility and schema changes

Maintaining backward compatibility and managing schema changes in GraphQL can be challenging, especially as the application and its data requirements evolve.

The schema defines the data structure and operations available to clients, including queries, mutations, and subscriptions. When changes are made to the schema—such as adding, renaming, or removing fields—they can have significant implications for existing clients that rely on those schema definitions.

Consider a GraphQL service that provides information about books. Initially, the schema might look like this:

type Book {
id: ID!
title: String!
author: String!
}

Clients are built to query this schema, expecting the author field to be a string:

query GetBooks {
books {
id
title
author
}
}

Later, the requirements change to provide more detailed information about authors. The author field is replaced with an author object that contains the firstName and lastName fields:

type Author {
firstName: String!
lastName: String!
}
type Book {
id: ID!
title: String!
author: Author!
}

This change breaks existing queries that expect author to be a string, which leads to backward compatibility issues.

Schema stitching and federation are two strategies designed to handle schema evolution and distributed systems in GraphQL. They help maintain backward compatibility and extend schemas in a scalable manner for improved performance.

Schema stitching allows for the merging of multiple GraphQL schemas into one. This is useful when you want to create a unified GraphQL API gateway that fronts several different services. It manages backward compatibility by allowing the original schema to remain in place while extending the overall schema with new services or types.

Assuming you have another service for book reviews with its own GraphQL schema:

type Review {
id: ID!
bookId: ID!
content: String!
rating: Int!
}

Using schema stitching, you can create a unified schema that includes both the original Book schema and the new Review schema without breaking existing clients:

// Stitched schema pseudocode
import { stitchSchemas } from '@graphql-tools/stitch';
const stitchedSchema = stitchSchemas({
schemas: [
bookSchema,
reviewSchema,
// Add a linking definition to associate books with reviews
`
extend type Book {
reviews: [Review]
}
`,
],
});
// This allows queries that fetch books along with their reviews
query GetBooksAndReviews {
books {
id
title
author {
firstName
lastName
}
reviews {
content
rating
}
}
}

The second strategy is GraphQL Federation, an approach designed specifically for distributed GraphQL architectures. It enables multiple GraphQL services to work together as a single data graph. This method avoids the need for a single monolithic schema, allowing each service to define its part of the overall schema.

Imagine separate services handle the book information and reviews. With federation, each service defines its part of the schema.

Here is the books service schema:

type Book @key(fields: "id") {
id: ID!
title: String!
author: Author!
}

Here is the reviews service schema:

type Review @key(fields: "bookId") {
id: ID!
bookId: ID!
content: String!
rating: Int!
}
extend type Book @key(fields: "id") {
id: ID! @external
reviews: [Review]
}

Federation allows these separate schemas to be combined into a cohesive data graph without centralizing the schema definition. This approach enables adding new services (like a new service for authors) without impacting existing ones, maintaining backward compatibility and allowing for more flexible schema evolution.

No standardized error handling

In traditional REST API, error handling is often standardized through HTTP status codes. For example, a 404 indicates a resource not found, a 500 indicates an internal server error, etc. Clients can rely on these standard codes to understand the nature of an error without needing to parse and interpret the error message itself.

However, the GraphQL ecosystem operates differently. It typically uses a single endpoint and HTTP POST method for all requests, and it returns a 200 OK status code for most GraphQL responses, even if the query contains errors.

This behavior means clients can't rely on HTTP status codes to understand what went wrong. Instead, GraphQL includes any errors in the response body alongside any data that could be retrieved. The lack of standardized error handling can make it difficult for clients to programmatically determine the nature of an error and decide how to handle it.

Let’s take a look at an example of GraphQL error response:

{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"]
}
]
}

This response indicates that the query failed partially (trying to fetch a user that doesn't exist) but doesn't follow a standard error code system. The client needs to parse the error message string, which can be fragile and not standardized across different GraphQL services.

Since GraphQL does not enforce a specific error-handling mechanism, developers are encouraged to implement their custom error-handling logic. This involves defining status fields, error codes, and error messages within the GraphQL schema to make error responses more predictable and useful.

By defining custom errors in the schema, developers can standardize error responses for their specific application or service. This approach allows clients to handle errors more effectively by checking these fields instead of relying on parsing error messages.

No built-in caching support

In REST APIs, caching mechanisms are well-established, often leveraging HTTP caching capabilities. These mechanisms can significantly reduce the number of requests to a server, thus improving load times and reducing server load.

In contrast, GraphQL operates over a single API endpoint using HTTP POST to send queries, making traditional HTTP caching techniques less effective. Because every query can be unique, the server must process each request, which can lead to increased load and slower response times.

For example, the following query:

query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
}
}

Each time you change the $userId, the server considers it a unique query, making it hard for traditional caching mechanisms to recognize and cache the response effectively.

To mitigate this, several strategies can be employed:

1. Client-side caching: Client-side libraries like Apollo Client offer built-in caching capabilities, storing the results of queries for reuse without needing to return to the server.

import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: 'your/graphql/endpoint',
cache: new InMemoryCache()
});
// Querying with Apollo Client automatically leverages the cache
client.query({
query: GetUser,
variables: { userId: "1" }
}).then(data => console.log(data));

Apollo Client's InMemoryCache can recognize when the result of a query can be reused, reducing the number of network requests.

2. GraphQL extensions: Extensions like persisted queries can help by mapping a query to a specific identifier. This allows caching at the HTTP layer because the same identifier always yields the same query. Client sends a hash of the query:

{
"id": "theHashOfTheQuery",
"variables": { "userId": "1" }
}

The server recognizes the hash and fetches the cached query result if available.

3. Custom resolver for cached data: Implementing custom logic in resolvers to fetch data from a cache, such as Redis, before querying the database.

const userResolver = async (parent, { userId }, context, info) => {
const cacheKey = `user:${userId}`;
let user = await redis.get(cacheKey);
if (!user) {
user = await database.getUserById(userId); // Pseudocode for fetching from DB
await redis.set(cacheKey, JSON.stringify(user));
}
return user;
};

This approach reduces database load by returning cached data when available.

4. HTTP caching headers: For queries that do not change frequently, HTTP caching headers can be set up with GET requests for GraphQL queries. Using GraphQL over GET and setting caching headers:

GET /graphql?query={query}&variables={variables}
Cache-Control: public, max-age=3600

This instructs the client and intermediary caches how long they should cache the response.

#How Hygraph overcomes these pain points

Hygraph is the first GraphQL-native, API-first headless CMS that effectively addresses several common GraphQL challenges. In doing so, it offers a vastly improved developer experience and reduced performance issues.

To counter the query complexity and steep learning curve associated with GraphQL, Hygraph provides extensive documentation, interactive tools like the API playground, and a supportive community. All this helps developers quickly master efficient query construction.

Concerns regarding exposing sensitive data are mitigated through a robust authorization system, allowing for granular access controls and ensuring data security. Hygraph employs Content Federation to handle schema changes and maintain backward compatibility, which enables seamless integration of new services without impacting existing operations.

Error handling in Hygraph follows the GraphQL specification, offering standardized error messages that facilitate quick diagnosis and resolution. Moreover, Hygraph tackles the issue of built-in caching through a sophisticated strategy involving globally distributed edge caches. This approach significantly accelerates content delivery speed and improves application scalability.

These features make Hygraph a powerful tool for overcoming GraphQL's pain points — and streamlining content management and development processes.

#Conclusion

This article presented a comprehensive overview of GraphQL, acknowledging its potential to revolutionize API development while providing practical solutions to overcome its limitations.

By adopting these strategies, developers can harness the full power of GraphQL, creating robust, flexible, and efficient applications.

Don't let GraphQL's complexities and common pitfalls hold you back. Hygraph makes setting up a CMS that fetches data with GraphQL is easy. Start building with Hygraph today and unlock the full potential of the first GraphQL native headless CMS.

Resources

The GraphQL Report 2024

Statistics and best practices from prominent GraphQL users.

Check out the report

Blog Author

Joel Olawanle

Joel Olawanle

Joel Olawanle is a Frontend Engineer and Technical writer based in Nigeria who is interested in making the web accessible to everyone by always looking for ways to give back to the tech community. He has a love for community building and open source.

Share with others

Sign up for our newsletter!

Be the first to know about releases and industry news and insights.