🎯 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.
1GET /users/123 → Returns full user object2GET /users/123/orders → Returns all orders3GET /users/123/profile → Returns full profileProblems:
1query {2 user(id: 123) {3 name4 email5 orders {6 id7 total8 }9 }10}Benefits:
Schema defines what data is available and how to query it.
1type User {2 id: ID!3 name: String!4 email: String!5 orders: [Order!]!6}7
8type Order {9 id: ID!10 total: Float!11 items: [OrderItem!]!12}13
14type Query {15 user(id: ID!): User16 users: [User!]!17}18
19type Mutation {20 createUser(name: String!, email: String!): User!21 updateUser(id: ID!, name: String): User!22}| 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.
1query {2 user(id: "123") {3 name4 email5 }6}Response:
1{2 "data": {3 "user": {4 "name": "John Doe",5 "email": "john@example.com"6 }7 }8}1query {2 users(limit: 10, offset: 0) {3 id4 name5 }6}1query {2 user(id: "123") {3 name4 email5 orders {6 id7 total8 items {9 product {10 name11 price12 }13 quantity14 }15 }16 }17}This single query replaces multiple REST calls:
GET /users/123GET /users/123/ordersGET /orders/456/itemsGET /products/789Get multiple versions of same field:
1query {2 user1: user(id: "123") {3 name4 }5 user2: user(id: "456") {6 name7 }8}Reusable field sets:
1fragment UserInfo on User {2 id3 name4 email5}6
7query {8 user(id: "123") {9 ...UserInfo10 orders {11 id12 }13 }14}Mutations are for creating, updating, or deleting data. Like POST/PUT/DELETE in REST.
1mutation {2 createUser(name: "Jane Doe", email: "jane@example.com") {3 id4 name5 email6 }7}Response:
1{2 "data": {3 "createUser": {4 "id": "456",5 "name": "Jane Doe",6 "email": "jane@example.com"7 }8 }9}1mutation {2 updateUser(id: "123", name: "John Smith") {3 id4 name5 email6 }7}1mutation {2 deleteUser(id: "123") {3 id4 }5}Execute multiple mutations in one request:
1mutation {2 createUser(name: "Alice", email: "alice@example.com") {3 id4 }5 createOrder(userId: "123", items: [...]) {6 id7 }8}Subscriptions provide real-time data using WebSockets.
1subscription {2 userUpdated(userId: "123") {3 id4 name5 email6 }7}How it works:
The biggest performance issue in GraphQL.
1query {2 users {3 name4 orders { # N+1 problem!5 id6 total7 }8 }9}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:
1from dataloader import DataLoader2import asyncio3
4# Create DataLoader for orders5order_loader = DataLoader(6 batch_load_fn=lambda user_ids: load_orders_for_users(user_ids)7)8
9async def get_user_with_orders(user_id):10 user = await get_user(user_id)11
12 # This will be batched!13 orders = await order_loader.load(user_id)14
15 return {16 "user": user,17 "orders": orders18 }19
20async def load_orders_for_users(user_ids):21 # Single query for all users22 orders = await db.query(23 "SELECT * FROM orders WHERE user_id IN ?",24 user_ids25 )26
27 # Group by user_id28 orders_by_user = {}29 for order in orders:30 if order.user_id not in orders_by_user:31 orders_by_user[order.user_id] = []32 orders_by_user[order.user_id].append(order)33
34 # Return in same order as requested35 return [orders_by_user.get(uid, []) for uid in user_ids]1import org.dataloader.DataLoader;2import org.dataloader.DataLoaderRegistry;3
4// Create DataLoader for orders5DataLoader<Integer, List<Order>> orderLoader = DataLoader6 .newDataLoader(userIds -> {7 // Batch load orders for all users8 return CompletableFuture.supplyAsync(() -> {9 List<Order> orders = orderRepository.findByUserIdIn(userIds);10
11 // Group by user_id12 Map<Integer, List<Order>> ordersByUser = orders.stream()13 .collect(Collectors.groupingBy(Order::getUserId));14
15 // Return in same order as requested16 return userIds.stream()17 .map(uid -> ordersByUser.getOrDefault(uid, Collections.emptyList()))18 .collect(Collectors.toList());19 });20 });21
22// Usage in resolver23public CompletableFuture<List<Order>> getOrders(User user) {24 // This will be batched!25 return orderLoader.load(user.getId());26}Resolvers are functions that fetch data for each field.
1from ariadne import QueryType, MutationType2from typing import Optional, List3
4query = QueryType()5mutation = MutationType()6
7@query.field("user")8def resolve_user(_, info, id: str) -> Optional[dict]:9 """Resolver for user query"""10 return user_repository.find_by_id(id)11
12@query.field("users")13def resolve_users(_, info) -> List[dict]:14 """Resolver for users query"""15 return user_repository.find_all()16
17@mutation.field("createUser")18def resolve_create_user(_, info, name: str, email: str) -> dict:19 """Resolver for createUser mutation"""20 user = user_repository.create(name=name, email=email)21 return user22
23# Field resolvers (for nested fields)24def resolve_user_orders(user: dict, info) -> List[dict]:25 """Resolver for User.orders field"""26 return order_repository.find_by_user_id(user["id"])1import com.coxautodev.graphql.tools.GraphQLQueryResolver;2import com.coxautodev.graphql.tools.GraphQLMutationResolver;3
4@Component5public class UserResolver implements GraphQLQueryResolver, GraphQLMutationResolver {6 private final UserRepository userRepository;7
8 public User user(String id) {9 // Resolver for user query10 return userRepository.findById(id).orElse(null);11 }12
13 public List<User> users() {14 // Resolver for users query15 return userRepository.findAll();16 }17
18 public User createUser(String name, String email) {19 // Resolver for createUser mutation20 User user = new User(name, email);21 return userRepository.save(user);22 }23}24
25// Field resolver for nested fields26@Component27public class UserFieldResolver implements GraphQLResolver<User> {28 private final OrderRepository orderRepository;29
30 public List<Order> orders(User user) {31 // Resolver for User.orders field32 return orderRepository.findByUserId(user.getId());33 }34}❌ Bad:
1query {2 users { # Could return millions!3 id4 name5 }6}✅ Good:
1query {2 users(first: 10, after: "cursor123") {3 edges {4 node {5 id6 name7 }8 }9 pageInfo {10 hasNextPage11 endCursor12 }13 }14}Prevent deeply nested queries:
1MAX_QUERY_DEPTH = 102
3def validate_query_depth(query, max_depth=MAX_QUERY_DEPTH):4 depth = calculate_depth(query)5 if depth > max_depth:6 raise GraphQLError("Query too deep")Authorize at field level:
1@query.field("user")2def resolve_user(_, info, id: str):3 user = user_repository.find_by_id(id)4
5 # Check if user can access email field6 if not can_access_field(info, "email"):7 user.pop("email") # Remove email from response8
9 return userPrevent expensive queries:
1def calculate_complexity(query):2 complexity = 03 for field in query.fields:4 complexity += field.complexity5 if field.has_list:6 complexity *= field.list_size7 return complexity8
9if calculate_complexity(query) > MAX_COMPLEXITY:10 raise GraphQLError("Query too complex")Disable introspection in production (or limit it):
1# Disable introspection2schema = make_executable_schema(type_defs, resolvers)3schema.introspection = False # In productionAt the code level, GraphQL translates to resolvers, schema definitions, and DataLoader patterns.
1from ariadne import make_executable_schema, QueryType, MutationType2from dataloader import DataLoader3
4# Schema definition5type_defs = """6type User {7 id: ID!8 name: String!9 email: String!10 orders: [Order!]!11}12
13type Order {14 id: ID!15 total: Float!16 items: [OrderItem!]!17}18
19type Query {20 user(id: ID!): User21 users: [User!]!22}23
24type Mutation {25 createUser(name: String!, email: String!): User!26}27"""28
29query = QueryType()30mutation = MutationType()31
32# DataLoaders33order_loader = DataLoader(34 batch_load_fn=lambda user_ids: load_orders_batch(user_ids)35)36
37@query.field("user")38def resolve_user(_, info, id: str):39 return user_repository.find_by_id(id)40
41@query.field("users")42def resolve_users(_, info):43 return user_repository.find_all()44
45@mutation.field("createUser")46def resolve_create_user(_, info, name: str, email: str):47 return user_repository.create(name=name, email=email)48
49# Field resolver with DataLoader50def resolve_user_orders(user, info):51 return order_loader.load(user["id"])52
53schema = make_executable_schema(type_defs, [query, mutation])1import graphql.GraphQL;2import graphql.schema.GraphQLSchema;3import com.coxautodev.graphql.tools.SchemaParser;4
5@Component6public class GraphQLService {7 private final GraphQL graphQL;8
9 public GraphQLService(UserResolver userResolver) {10 // Schema file: schema.graphqls11 GraphQLSchema schema = SchemaParser.newParser()12 .file("schema.graphqls")13 .resolvers(userResolver)14 .build()15 .makeExecutableSchema();16
17 this.graphQL = GraphQL.newGraphQL(schema)18 .queryExecutionStrategy(new BatchedExecutionStrategy())19 .build();20 }21
22 public ExecutionResult execute(String query) {23 return graphQL.execute(query);24 }25}🎯 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.