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:
- Fix the N+1 scenario using DataLoader.
- Implement a query depth limit.
- 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.
GO TO FULL VERSION