Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
GraphQL is a query language for APIs and a runtime for executing those queries with your existing data. It provides a complete and understandable description of the data in your API and gives clients the power to ask for exactly what they need.
GraphQL, despite its flexibility and efficiency, has several common anti-patterns that can lead to performance issues, security vulnerabilities, and maintainability problems. Here are the most important anti-patterns to avoid when working with GraphQL.
# Anti-pattern: Fetching all items without pagination
query GetAllUsers {
users {
id
name
email
posts {
id
title
content
comments {
id
text
author {
id
name
}
}
}
}
}
# Better approach: Use pagination
query GetPaginatedUsers($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
email
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
// Resolver implementation with pagination
const resolvers = {
Query: {
users: async (_, { first = 10, after }) => {
const users = await fetchPaginatedUsers(first, after);
return {
edges: users.map(user => ({
node: user,
cursor: encodeCursor(user.id)
})),
pageInfo: {
hasNextPage: users.length === first,
endCursor: users.length ? encodeCursor(users[users.length - 1].id) : null
}
};
}
}
};
Fetching large collections without pagination can lead to performance issues and timeout errors. Implement cursor-based or offset-based pagination for all list queries.
// Anti-pattern: Overfetching in resolvers
const resolvers = {
User: {
posts: async (parent) => {
// Fetches ALL posts for a user, even if the client only needs a few fields
return await Post.find({ userId: parent.id });
}
}
};
// Better approach: Use dataloader for batching and caching
const postLoader = new DataLoader(async (userIds) => {
const posts = await Post.find({ userId: { $in: userIds } });
// Group posts by userId
const postsByUserId = {};
posts.forEach(post => {
if (!postsByUserId[post.userId]) {
postsByUserId[post.userId] = [];
}
postsByUserId[post.userId].push(post);
});
// Return posts in the same order as userIds
return userIds.map(userId => postsByUserId[userId] || []);
});
const resolvers = {
User: {
posts: async (parent) => {
return await postLoader.load(parent.id);
}
}
};
Overfetching in resolvers can lead to N+1 query problems and poor performance. Use DataLoader to batch and cache database queries.
// Anti-pattern: No authorization in resolvers
const resolvers = {
Query: {
user: async (_, { id }) => {
return await User.findById(id);
}
},
Mutation: {
updateUser: async (_, { id, input }) => {
const user = await User.findByIdAndUpdate(id, input, { new: true });
return user;
}
}
};
// Better approach: Implement authorization in resolvers
const resolvers = {
Query: {
user: async (_, { id }, context) => {
// Check if user is authenticated
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
// Check if user has permission to view this user
if (context.user.id !== id && !context.user.isAdmin) {
throw new ForbiddenError('You do not have permission to view this user');
}
return await User.findById(id);
}
},
Mutation: {
updateUser: async (_, { id, input }, context) => {
// Check if user is authenticated
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
// Check if user has permission to update this user
if (context.user.id !== id && !context.user.isAdmin) {
throw new ForbiddenError('You do not have permission to update this user');
}
const user = await User.findByIdAndUpdate(id, input, { new: true });
return user;
}
}
};
Not implementing proper authorization in resolvers can lead to security vulnerabilities. Implement authorization checks in every resolver that accesses sensitive data.
// Anti-pattern: No input validation
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
// No validation before creating user
const user = new User(input);
await user.save();
return user;
}
}
};
// Better approach: Validate inputs
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
// Validate input
const { error } = userSchema.validate(input);
if (error) {
throw new UserInputError('Invalid input', { details: error.details });
}
// Check if email is already taken
const existingUser = await User.findOne({ email: input.email });
if (existingUser) {
throw new UserInputError('Email already taken');
}
const user = new User(input);
await user.save();
return user;
}
}
};
Not validating user input can lead to data corruption and security vulnerabilities. Implement input validation for all mutations.
// Anti-pattern: No query complexity analysis
const server = new ApolloServer({
typeDefs,
resolvers
});
// Better approach: Implement query complexity analysis
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 10,
listFactor: 10
})
]
});
Without query complexity analysis, clients can send expensive queries that overload your server. Implement query complexity analysis to prevent DoS attacks.
// Anti-pattern: Poor error handling
const resolvers = {
Query: {
user: async (_, { id }) => {
try {
return await User.findById(id);
} catch (error) {
console.error(error);
return null; // Silently fails
}
}
}
};
// Better approach: Proper error handling
const resolvers = {
Query: {
user: async (_, { id }) => {
try {
const user = await User.findById(id);
if (!user) {
throw new UserInputError(`User with ID ${id} not found`);
}
return user;
} catch (error) {
// Log the error for internal tracking
console.error('Error fetching user:', error);
// Rethrow Apollo errors
if (error instanceof ApolloError) {
throw error;
}
// Convert other errors to appropriate GraphQL errors
if (error.name === 'CastError') {
throw new UserInputError(`Invalid ID format: ${id}`);
}
// For unexpected errors, return a generic message in production
throw new ApolloError(
process.env.NODE_ENV === 'production'
? 'Internal server error'
: error.message
);
}
}
}
};
Poor error handling can lead to security vulnerabilities and a poor developer experience. Implement proper error handling in all resolvers.
// Anti-pattern: No field-level permissions
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
role: String!
salary: Float
ssn: String
}
`;
// Better approach: Implement field-level permissions
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
role: String!
salary: Float @auth(requires: ADMIN)
ssn: String @auth(requires: OWNER)
}
`;
// Custom directive implementation
const resolvers = {
User: {
salary: async (parent, args, context) => {
if (!context.user || context.user.role !== 'ADMIN') {
return null;
}
return parent.salary;
},
ssn: async (parent, args, context) => {
if (!context.user || (context.user.id !== parent.id && context.user.role !== 'ADMIN')) {
return null;
}
return parent.ssn;
}
}
};
Not implementing field-level permissions can lead to sensitive data exposure. Use directives or resolver-level checks to implement field-level permissions.
# Anti-pattern: Duplicated fields across queries
query GetUser {
user(id: "123") {
id
name
email
profilePicture
bio
}
}
query GetUserPosts {
user(id: "123") {
id
name
email
profilePicture
bio
posts {
id
title
content
}
}
}
# Better approach: Use fragments
fragment UserDetails on User {
id
name
email
profilePicture
bio
}
query GetUser {
user(id: "123") {
...UserDetails
}
}
query GetUserPosts {
user(id: "123") {
...UserDetails
posts {
id
title
content
}
}
}
Duplicating field selections across queries leads to maintenance issues. Use fragments to share field selections across queries.
// Anti-pattern: No caching strategy
const server = new ApolloServer({
typeDefs,
resolvers
});
// Better approach: Implement caching
const server = new ApolloServer({
typeDefs,
resolvers,
cache: new RedisCache({
host: 'redis-server',
port: 6379
}),
cacheControl: {
defaultMaxAge: 60, // 1 minute
calculateHttpHeaders: true
}
});
// Type definitions with cache hints
const typeDefs = gql`
type User @cacheControl(maxAge: 300) {
id: ID!
name: String!
email: String!
posts: [Post!]! @cacheControl(maxAge: 60)
}
type Post @cacheControl(maxAge: 1800) {
id: ID!
title: String!
content: String!
}
`;
Not implementing proper caching can lead to poor performance. Use cache control directives and a caching solution like Redis to improve performance.
// Anti-pattern: Not using persisted queries
// Client sends full query text with each request
const client = new ApolloClient({
uri: '/graphql'
});
// Better approach: Use persisted queries
// Server setup
const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
cache: new RedisCache({
host: 'redis-server',
port: 6379
})
}
});
// Client setup
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache(),
link: createPersistedQueryLink().concat(httpLink)
});
Sending full query text with each request increases network traffic. Use persisted queries to reduce network traffic and improve security.
// Anti-pattern: Manual type definitions
// Client code with manual type definitions
interface User {
id: string;
name: string;
email: string;
}
function fetchUser(id: string): Promise<User> {
return client.query({
query: gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id }
}).then(result => result.data.user);
}
// Better approach: Use code generation
// With GraphQL Code Generator
// codegen.yml
/*
schema: http://localhost:4000/graphql
documents: ./src/**/*.graphql
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
*/
// query.graphql
/*
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
*/
// Client code with generated types
import { useGetUserQuery } from './generated/graphql';
function UserProfile({ id }: { id: string }) {
const { data, loading, error } = useGetUserQuery({
variables: { id }
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
</div>
);
}
Manually defining types for GraphQL operations is error-prone. Use code generation tools like GraphQL Code Generator to generate type-safe code from your GraphQL schema and operations.
// Anti-pattern: Monolithic GraphQL schema
const typeDefs = gql`
type User {
id: ID!
name: String!
orders: [Order!]!
cart: Cart
wishlist: [Product!]!
reviews: [Review!]!
}
type Product {
id: ID!
name: String!
price: Float!
inventory: Inventory!
reviews: [Review!]!
}
# Many more types for different domains
`;
// Better approach: Use schema federation
// User service
const userTypeDefs = gql`
type User @key(fields: "id") {
id: ID!
name: String!
}
`;
// Order service
const orderTypeDefs = gql`
type User @key(fields: "id") @extends {
id: ID! @external
orders: [Order!]!
}
type Order {
id: ID!
products: [Product!]!
total: Float!
}
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
}
`;
// Product service
const productTypeDefs = gql`
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
inventory: Inventory!
}
type Inventory {
quantity: Int!
warehouse: String!
}
`;
// Review service
const reviewTypeDefs = gql`
type User @key(fields: "id") @extends {
id: ID! @external
reviews: [Review!]!
}
type Product @key(fields: "id") @extends {
id: ID! @external
reviews: [Review!]!
}
type Review {
id: ID!
text: String!
rating: Int!
user: User!
product: Product!
}
`;
A monolithic GraphQL schema becomes hard to maintain as your application grows. Use schema stitching or federation to split your schema across multiple services.