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:
Resolvers are functions that fetch data for each field.
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.
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.