CodeGym /Courses /Module 5. Spring /Lecture 300: Common mistakes when building a GraphQL API

Lecture 300: Common mistakes when building a GraphQL API

Module 5. Spring
Level 16 , Lesson 9
Available

GraphQL is a powerful tool for building APIs, but as you know, with great power comes great responsibility. Even the most experienced developers make mistakes when designing GraphQL schemas, data handlers, and queries. In this lecture we'll break down the most common mistakes, show how to avoid them, and review best practices for writing a high-quality, performant GraphQL API.


Typical mistakes when designing the schema

Incomplete or overexposed schema

Schema design is kind of an art, and it's easy to either overdo it or, on the flip side, create a schema that doesn't cover the needed functionality.

Example mistake:


type Query {
  userProfile: UserProfile
}

type UserProfile {
  id: ID
  firstName: String
  lastName: String
  age: Int
  hobbies: [String]
  contactInfo: String
}

Looks fine, but what if contactInfo contains sensitive data? That kind of schema can easily lead to a data leak.

How to avoid it:

  • Design minimal, isolated schemas.
  • Use a need-to-know approach when exposing data and avoid "overfeeding" clients with information.

Fixed schema example:


type Query {
  userProfile: UserProfile
}

type UserProfile {
  id: ID
  firstName: String
  lastName: String
  age: Int
  hobbies: [String]
}

type PrivateUserInfo {
  contactInfo: String
}

Now the sensitive info is moved to a separate type that can be provided only to authorized users.

Deep nested structures (N+1 problem)

Error:

If your schema contains very deep nesting, a client can request huge amounts of data in a single powerful query.

Example:


query {
  users {
    id
    posts {
      id
      comments {
        id
        text
      }
    }
  }
}

If there are thousands of users, each with dozens of posts, and each post has hundreds of comments, this query can bring your server down.

How to avoid it:

  • Limit query depth via plugins or middleware.
  • Use Batch Loading to eliminate duplicate requests.

Data handling mistakes

Performance problems (N+1 issue)

Problem: A common mistake when using GraphQL is that fetching related data causes database queries to fire like a machine gun.

Example:


@QueryMapping
public List<User> users() {
    return userService.getAllUsers();
}

@QueryMapping
public List<Post> posts(@Argument("userId") String userId) {
    return postService.getPostsByUser(userId);
}

Each user may trigger a separate query to fetch related posts. If there are 100 users, that means 100 SQL queries to the database.

Fix: Use DataLoader:


@Bean
public DataLoaderRegistry dataLoaderRegistry() {
    DataLoaderRegistry registry = new DataLoaderRegistry();

    DataLoader<String, List<Post>> postLoader = DataLoader.newMappedDataLoader(userService::getPostsForUsers);
    registry.register("posts", postLoader);

    return registry;
}

Now for 100 users you'll run just one batch query.

Unhandled exceptions

Error: your data handler throws unhandled exceptions that end up in the server log and can potentially reveal internal system details.

Example:

@QueryMapping
public User user(@Argument("id") String id) {
    return userService.findById(id);
}

If a user with the given id isn't found, you're likely to get a NullPointerException.

Solution: Wrap errors into custom exceptions.


@QueryMapping
public User user(@Argument("id") String id) {
    return userService.findById(id).orElseThrow(() -> new CustomException("User not found"));
}

Data protection mistakes

No limits on query depth and complexity

GraphQL lets clients be so flexible that naughty actors can send requests with depth of 1000 levels or complexity exceeding 100k operations.

Solution:

  • Enable limits on query depth and complexity.
  • Use libraries like graphql-java or plugins to configure limits.

GraphQLSchema schema = GraphQLSchema.newSchema()
    .query(QueryType)
    .maxQueryDepth(10)
    .maxQueryComplexity(1000)
    .build();

Missing authorization and authentication checks

Error: People often forget to check user authorization when processing requests.

Solution:

Use DataFetchEnvironment to validate the current user:


public CompletableFuture<User> user(DataFetchingEnvironment env) {
    String token = env.getContext();
    if (!authService.isAuthenticated(token)) {
        throw new AuthenticationException("Invalid token");
    }
    return userService.getUserByToken(token);
}

Testing mistakes

Not covering complex queries with tests

Many people limit tests to simple queries and forget to test queries with fragments, nested fields, and mutations.

Solution:

Create tests for all use cases. For example, use MockMvc for integration tests:


mockMvc.perform(post("/graphql")
        .content("{\"query\":\"{ user { id, name } }\"}")
        .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.data.user.id").exists());

API design mistakes

Duplicating logic in schemas and resolvers

Error: Logic gets duplicated for the same behavior across the GraphQL API.

Solution:

Use a service layer to handle business logic and adapt it to the resolver's request context.


Practice: fixing mistakes

To wrap up, let's look at some exercises:

  1. Fix the N+1 scenario using DataLoader.
  2. Implement a query depth limit.
  3. Write integration tests for a mutation with nested objects.

On a real project this will give you a more robust API, and your clients will thank you.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION