🎯 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 for APIs that lets clients request exactly the data they need. Unlike REST where you get fixed responses, GraphQL gives you the power to shape your queries.
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! 😱
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());}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()); }}❌ Bad:
query { users { # Could return millions! id name }}✅ 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); }}🎯 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.