Ask for What You Need
GraphQL lets clients specify exactly what data they need, reducing over-fetching and under-fetching.
GraphQL is a query language and runtime for APIs developed by Facebook (now Meta) in 2012 and open-sourced in 2015. Unlike REST APIs that return fixed data structures, GraphQL allows clients to specify exactly what data they need, reducing over-fetching and under-fetching.
Traditional REST APIs have a fundamental limitation: they return fixed data structures. If you need user data, you call /users/123 and get all user fields—even if you only need the name. If you need related data (like a user’s orders), you make multiple requests. This leads to:
GraphQL solves this by providing a single endpoint where clients can request exactly the data they need, including related data, in a single request.
GET /users/123 → Returns full user objectGET /users/123/orders → Returns all ordersGET /users/123/profile → Returns full profileProblems:
query { user(id: 123) { name email orders { id total } }}Benefits:
Schema defines what data is available and how to query it.
type User { id: ID! name: String! email: String! orders: [Order!]!}
type Order { id: ID! total: Float! items: [OrderItem!]!}
type Query { user(id: ID!): User users: [User!]!}
type Mutation { createUser(name: String!, email: String!): User! updateUser(id: ID!, name: String): User!}| Type | Meaning | Example |
|---|---|---|
String | Text | "John Doe" |
Int | Integer | 42 |
Float | Decimal | 99.99 |
Boolean | True/False | true |
ID | Unique identifier | "123" |
! | Required (non-null) | String! |
[Type] | Array | [String!]! |
Queries are for reading data. They’re like GET requests in REST.
query { user(id: "123") { name email }}Response:
{ "data": { "user": { "name": "John Doe", } }}query { users(limit: 10, offset: 0) { id name }}query { user(id: "123") { name email orders { id total items { product { name price } quantity } } }}This single query replaces multiple REST calls:
GET /users/123GET /users/123/ordersGET /orders/456/itemsGET /products/789Get multiple versions of same field:
query { user1: user(id: "123") { name } user2: user(id: "456") { name }}Reusable field sets:
fragment UserInfo on User { id name email}
query { user(id: "123") { ...UserInfo orders { id } }}Mutations are for creating, updating, or deleting data. Like POST/PUT/DELETE in REST.
mutation { id name email }}Response:
{ "data": { "createUser": { "id": "456", "name": "Jane Doe", } }}mutation { updateUser(id: "123", name: "John Smith") { id name email }}mutation { deleteUser(id: "123") { id }}Execute multiple mutations in one request:
mutation { id } createOrder(userId: "123", items: [...]) { id }}Subscriptions provide real-time data using WebSockets.
subscription { userUpdated(userId: "123") { id name email }}How it works:
The biggest performance issue in GraphQL.
query { users { name orders { # N+1 problem! id total } }}What happens:
SELECT * FROM users (gets 100 users)SELECT * FROM orders WHERE user_id = 1SELECT * FROM orders WHERE user_id = 2Total: 1 + 100 = 101 queries! This is the N+1 query problem—one query to get the list, then N queries (one per item) to get related data.
DataLoader batches requests:
How DataLoader works:
from dataloader import DataLoaderimport asyncio
# Create DataLoader for ordersorder_loader = DataLoader( batch_load_fn=lambda user_ids: load_orders_for_users(user_ids))
async def get_user_with_orders(user_id): user = await get_user(user_id)
# This will be batched! orders = await order_loader.load(user_id)
return { "user": user, "orders": orders }
async def load_orders_for_users(user_ids): # Single query for all users orders = await db.query( "SELECT * FROM orders WHERE user_id IN ?", user_ids )
# Group by user_id orders_by_user = {} for order in orders: if order.user_id not in orders_by_user: orders_by_user[order.user_id] = [] orders_by_user[order.user_id].append(order)
# Return in same order as requested return [orders_by_user.get(uid, []) for uid in user_ids]import org.dataloader.DataLoader;import org.dataloader.DataLoaderRegistry;
// Create DataLoader for ordersDataLoader<Integer, List<Order>> orderLoader = DataLoader .newDataLoader(userIds -> { // Batch load orders for all users return CompletableFuture.supplyAsync(() -> { List<Order> orders = orderRepository.findByUserIdIn(userIds);
// Group by user_id Map<Integer, List<Order>> ordersByUser = orders.stream() .collect(Collectors.groupingBy(Order::getUserId));
// Return in same order as requested return userIds.stream() .map(uid -> ordersByUser.getOrDefault(uid, Collections.emptyList())) .collect(Collectors.toList()); }); });
// Usage in resolverpublic CompletableFuture<List<Order>> getOrders(User user) { // This will be batched! return orderLoader.load(user.getId());}import DataLoader from 'dataloader';
// Create DataLoader for ordersconst orderLoader = new DataLoader<number, Order[]>( async (userIds: readonly number[]) => { // Batch load orders for all users const orders = await orderRepository.findByUserIdIn([...userIds]);
// Group by user_id const ordersByUser = new Map<number, Order[]>(); orders.forEach(order => { if (!ordersByUser.has(order.userId)) { ordersByUser.set(order.userId, []); } ordersByUser.get(order.userId)!.push(order); });
// Return in same order as requested return userIds.map(userId => ordersByUser.get(userId) || []); });
// Usage in resolverasync function getUserWithOrders(userId: number) { const user = await getUser(userId);
// This will be batched! const orders = await orderLoader.load(userId);
return { user, orders };}#include <vector>#include <unordered_map>#include <future>
class OrderDataLoader { // DataLoader for batching order requestsprivate: OrderRepository& orderRepository; std::unordered_map<int, std::shared_future<std::vector<Order>>> pending;
public: OrderDataLoader(OrderRepository& orderRepository) : orderRepository(orderRepository) {}
std::vector<Order> load(int userId) { // This will be batched auto it = pending.find(userId); if (it == pending.end()) { // Create future for this user auto future = std::async(std::launch::async, [this, userId]() { return orderRepository.findByUserId(userId); }); pending[userId] = future.share(); return future.get(); } return it->second.get(); }
void batchLoad(const std::vector<int>& userIds) { // Batch load all orders at once auto orders = orderRepository.findByUserIdIn(userIds);
// Group by user_id std::unordered_map<int, std::vector<Order>> ordersByUser; for (const auto& order : orders) { ordersByUser[order.getUserId()].push_back(order); }
// Store results for (int userId : userIds) { auto future = std::async(std::launch::deferred, [ordersByUser, userId]() { return ordersByUser[userId]; }); pending[userId] = future.share(); } }};using DataLoader;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;
public class OrderDataLoader { // DataLoader for batching order requests private readonly DataLoader<int, List<Order>> orderLoader;
public OrderDataLoader(OrderRepository orderRepository) { this.orderLoader = new DataLoader<int, List<Order>>( async userIds => { // Batch load orders for all users var orders = await orderRepository.FindByUserIdInAsync(userIds.ToList());
// Group by user_id var ordersByUser = orders .GroupBy(o => o.UserId) .ToDictionary(g => g.Key, g => g.ToList());
// Return in same order as requested return userIds.Select(userId => ordersByUser.GetValueOrDefault(userId, new List<Order>()) ).ToList(); } ); }
public async Task<List<Order>> LoadAsync(int userId) { // This will be batched! return await orderLoader.LoadAsync(userId); }}
// Usage in resolverpublic async Task<UserWithOrders> GetUserWithOrdersAsync(int userId) { var user = await userRepository.FindByIdAsync(userId);
// This will be batched! var orders = await orderDataLoader.LoadAsync(userId);
return new UserWithOrders { User = user, Orders = orders };}Resolvers are functions that fetch data for each field.
from ariadne import QueryType, MutationTypefrom typing import Optional, List
query = QueryType()mutation = MutationType()
@query.field("user")def resolve_user(_, info, id: str) -> Optional[dict]: """Resolver for user query""" return user_repository.find_by_id(id)
@query.field("users")def resolve_users(_, info) -> List[dict]: """Resolver for users query""" return user_repository.find_all()
@mutation.field("createUser")def resolve_create_user(_, info, name: str, email: str) -> dict: """Resolver for createUser mutation""" user = user_repository.create(name=name, email=email) return user
# Field resolvers (for nested fields)def resolve_user_orders(user: dict, info) -> List[dict]: """Resolver for User.orders field""" return order_repository.find_by_user_id(user["id"])import com.coxautodev.graphql.tools.GraphQLQueryResolver;import com.coxautodev.graphql.tools.GraphQLMutationResolver;
@Componentpublic class UserResolver implements GraphQLQueryResolver, GraphQLMutationResolver { private final UserRepository userRepository;
public User user(String id) { // Resolver for user query return userRepository.findById(id).orElse(null); }
public List<User> users() { // Resolver for users query return userRepository.findAll(); }
public User createUser(String name, String email) { // Resolver for createUser mutation User user = new User(name, email); return userRepository.save(user); }}
// Field resolver for nested fields@Componentpublic class UserFieldResolver implements GraphQLResolver<User> { private final OrderRepository orderRepository;
public List<Order> orders(User user) { // Resolver for User.orders field return orderRepository.findByUserId(user.getId()); }}import { GraphQLResolveInfo } from 'graphql';
const resolvers = { Query: { user: async (_: any, { id }: { id: string }) => { // Resolver for user query return await userRepository.findById(id); }, users: async () => { // Resolver for users query return await userRepository.findAll(); } }, Mutation: { createUser: async (_: any, { name, email }: { name: string, email: string }) => { // Resolver for createUser mutation return await userRepository.create({ name, email }); } }, User: { orders: async (user: User) => { // Field resolver for User.orders field return await orderRepository.findByUserId(user.id); } }};
export default resolvers;#include <string>#include <vector>#include <optional>
class GraphQLResolvers {private: UserRepository& userRepository; OrderRepository& orderRepository;
public: GraphQLResolvers(UserRepository& userRepo, OrderRepository& orderRepo) : userRepository(userRepo), orderRepository(orderRepo) {}
std::optional<User> resolveUser(const std::string& id) { // Resolver for user query return userRepository.findById(id); }
std::vector<User> resolveUsers() { // Resolver for users query return userRepository.findAll(); }
User resolveCreateUser(const std::string& name, const std::string& email) { // Resolver for createUser mutation return userRepository.create(name, email); }
std::vector<Order> resolveUserOrders(const User& user) { // Field resolver for User.orders field return orderRepository.findByUserId(user.getId()); }};using GraphQL.Types;
public class UserQuery : ObjectGraphType { public UserQuery(UserRepository userRepository) { Field<UserType>( "user", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "id" } ), resolve: context => { // Resolver for user query string id = context.GetArgument<string>("id"); return userRepository.FindById(id); } );
Field<ListGraphType<UserType>>( "users", resolve: context => { // Resolver for users query return userRepository.FindAll(); } ); }}
public class UserMutation : ObjectGraphType { public UserMutation(UserRepository userRepository) { Field<UserType>( "createUser", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "name" }, new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "email" } ), resolve: context => { // Resolver for createUser mutation string name = context.GetArgument<string>("name"); string email = context.GetArgument<string>("email"); return userRepository.Create(name, email); } ); }}
public class UserType : ObjectGraphType<User> { public UserType(OrderRepository orderRepository) { Field(x => x.Id); Field(x => x.Name); Field(x => x.Email); Field<ListGraphType<OrderType>>( "orders", resolve: context => { // Field resolver for User.orders field return orderRepository.FindByUserId(context.Source.Id); } ); }}Bad:
query { users { # Could return millions! id name }}Why it’s bad: Without pagination, a query could return millions of records, causing performance issues, memory problems, and timeouts.
Good:
query { users(first: 10, after: "cursor123") { edges { node { id name } } pageInfo { hasNextPage endCursor } }}Prevent deeply nested queries:
MAX_QUERY_DEPTH = 10
def validate_query_depth(query, max_depth=MAX_QUERY_DEPTH): depth = calculate_depth(query) if depth > max_depth: raise GraphQLError("Query too deep")Authorize at field level:
@query.field("user")def resolve_user(_, info, id: str): user = user_repository.find_by_id(id)
# Check if user can access email field if not can_access_field(info, "email"): user.pop("email") # Remove email from response
return userPrevent expensive queries:
def calculate_complexity(query): complexity = 0 for field in query.fields: complexity += field.complexity if field.has_list: complexity *= field.list_size return complexity
if calculate_complexity(query) > MAX_COMPLEXITY: raise GraphQLError("Query too complex")Disable introspection in production (or limit it):
# Disable introspectionschema = make_executable_schema(type_defs, resolvers)schema.introspection = False # In productionAt the code level, GraphQL translates to resolvers, schema definitions, and DataLoader patterns.
from ariadne import make_executable_schema, QueryType, MutationTypefrom dataloader import DataLoader
# Schema definitiontype_defs = """type User { id: ID! name: String! email: String! orders: [Order!]!}
type Order { id: ID! total: Float! items: [OrderItem!]!}
type Query { user(id: ID!): User users: [User!]!}
type Mutation { createUser(name: String!, email: String!): User!}"""
query = QueryType()mutation = MutationType()
# DataLoadersorder_loader = DataLoader( batch_load_fn=lambda user_ids: load_orders_batch(user_ids))
@query.field("user")def resolve_user(_, info, id: str): return user_repository.find_by_id(id)
@query.field("users")def resolve_users(_, info): return user_repository.find_all()
@mutation.field("createUser")def resolve_create_user(_, info, name: str, email: str): return user_repository.create(name=name, email=email)
# Field resolver with DataLoaderdef resolve_user_orders(user, info): return order_loader.load(user["id"])
schema = make_executable_schema(type_defs, [query, mutation])import graphql.GraphQL;import graphql.schema.GraphQLSchema;import com.coxautodev.graphql.tools.SchemaParser;
@Componentpublic class GraphQLService { private final GraphQL graphQL;
public GraphQLService(UserResolver userResolver) { // Schema file: schema.graphqls GraphQLSchema schema = SchemaParser.newParser() .file("schema.graphqls") .resolvers(userResolver) .build() .makeExecutableSchema();
this.graphQL = GraphQL.newGraphQL(schema) .queryExecutionStrategy(new BatchedExecutionStrategy()) .build(); }
public ExecutionResult execute(String query) { return graphQL.execute(query); }}import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLList, GraphQLID, buildSchema } from 'graphql';import { graphqlHTTP } from 'express-graphql';import DataLoader from 'dataloader';
// Schema definitionconst typeDefs = `type User { id: ID! name: String! email: String! orders: [Order!]!}
type Order { id: ID! total: Float! items: [OrderItem!]!}
type Query { user(id: ID!): User users: [User!]!}
type Mutation { createUser(name: String!, email: String!): User!}`;
// DataLoadersconst orderLoader = new DataLoader<number, Order[]>( async (userIds: readonly number[]) => { const orders = await orderRepository.findByUserIdIn([...userIds]); const ordersByUser = new Map<number, Order[]>(); orders.forEach(order => { if (!ordersByUser.has(order.userId)) { ordersByUser.set(order.userId, []); } ordersByUser.get(order.userId)!.push(order); }); return userIds.map(userId => ordersByUser.get(userId) || []); });
// Resolversconst resolvers = { Query: { user: async (_: any, { id }: { id: string }) => { return await userRepository.findById(id); }, users: async () => { return await userRepository.findAll(); } }, Mutation: { createUser: async (_: any, { name, email }: { name: string, email: string }) => { return await userRepository.create({ name, email }); } }, User: { orders: async (user: User) => { return await orderLoader.load(user.id); } }};
const schema = buildSchema(typeDefs);#include <string>#include <vector>
class GraphQLService {private: GraphQLSchema schema; UserRepository& userRepository; OrderRepository& orderRepository; OrderDataLoader orderLoader;
public: GraphQLService(UserRepository& userRepo, OrderRepository& orderRepo) : userRepository(userRepo), orderRepository(orderRepo), orderLoader(orderRepo) { // Build schema schema = buildSchema(); }
ExecutionResult execute(const std::string& query) { // Parse and execute GraphQL query auto parsedQuery = parseQuery(query); return executeQuery(parsedQuery); }
private: GraphQLSchema buildSchema() { // Schema definition (simplified) // In production, use a GraphQL library return GraphQLSchema(); }
ExecutionResult executeQuery(const ParsedQuery& query) { // Execute query using resolvers // Simplified implementation return ExecutionResult(); }};using GraphQL;using GraphQL.Types;
public class GraphQLService { // GraphQL service implementation private readonly GraphQLSchema schema;
public GraphQLService(UserRepository userRepository, OrderRepository orderRepository) { // Build schema var schemaBuilder = new SchemaBuilder();
schemaBuilder.Query = new UserQuery(userRepository); schemaBuilder.Mutation = new UserMutation(userRepository);
// Register types schemaBuilder.Types.Add(new UserType(orderRepository)); schemaBuilder.Types.Add(new OrderType());
this.schema = schemaBuilder.Build(); }
public ExecutionResult Execute(string query) { // Execute GraphQL query var executor = new DocumentExecutor(); var document = new GraphQLParser().Parse(query);
return executor.ExecuteAsync(new ExecutionOptions { Schema = schema, Query = query, Document = document }).Result; }}Ask for What You Need
GraphQL lets clients specify exactly what data they need, reducing over-fetching and under-fetching.
Watch for N+1
The N+1 query problem is GraphQL’s biggest performance issue. Use DataLoader to batch requests.
Resolvers Fetch Data
Resolvers are functions that fetch data for each field. They’re where your business logic lives.
Schema is Contract
GraphQL schema defines your API contract. It’s self-documenting and strongly typed.