GraphQL & API Design Patterns
Schema design, resolvers, federation, and advanced querying
IT & Tech Reports | Class IM24A | 2026
1. GraphQL Fundamentals Recap
GraphQL is a query language for APIs developed by Facebook in 2012 and open-sourced in 2015. The
central idea: the client specifies exactly what data it needs in a single request — no over-fetching
(getting unused fields) and no under-fetching (needing multiple requests). Every GraphQL API exposes
a single endpoint and a strongly typed schema.
REST GraphQL
Multiple endpoints (/users, /posts, /comments) Single endpoint (/graphql)
Server controls response shape Client controls response shape
Multiple requests for related data Single request with nested queries
Versioning via URL (/v1/, /v2/) Evolve schema — deprecate fields
HTTP caching built-in Requires client-side cache (Apollo, urql)
Simple debugging (curl) Needs GraphQL playground / introspection
2. Schema Definition Language (SDL)
# Full schema example for a school application
scalar DateTime
scalar JSON
# Enums
enum Role { STUDENT TEACHER ADMIN }
enum Status { ACTIVE INACTIVE SUSPENDED }
# Interfaces
interface Node {
id: ID!
}
interface Timestamps {
createdAt: DateTime!
updatedAt: DateTime!
}
# Types
type User implements Node & Timestamps {
id: ID!
name: String!
email: String!
role: Role!
status: Status!
class: Class
grades: [Grade!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Class implements Node {
id: ID!
name: String! # e.g. "IM24A"
teacher: User!
students: [User!]!
subjects: [Subject!]!
}
type Grade {
subject: Subject!
score: Float!
date: DateTime!
comment: String
}
type Subject {
id: ID!
name: String!
code: String!
}
# Queries — read operations
type Query {
me: User
user(id: ID!): User
users(
filter: UserFilterInput
orderBy: UserOrderByInput
first: Int = 20
after: String
): UserConnection!
class(id: ID!): Class
}
# Mutations — write operations
type Mutation {
createUser(input: CreateUserInput!): UserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
deleteUser(id: ID!): DeletePayload!
addGrade(userId: ID!, input: GradeInput!): GradePayload!
}
# Subscriptions — real-time
type Subscription {
gradeAdded(classId: ID!): Grade!
userStatusChanged(userId: ID!): User!
}
# Input types
input CreateUserInput {
name: String!
email: String!
role: Role! = STUDENT
classId: ID
}
input UserFilterInput {
role: Role
status: Status
classId: ID
search: String
}
# Pagination (Relay cursor spec)
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge { node: User!; cursor: String! }
type PageInfo { hasNextPage: Boolean!; endCursor: String }
# Payload types with errors
type UserPayload {
user: User
errors: [UserError!]!
}
type UserError { field: String; message: String! }
type DeletePayload { success: Boolean!; id: ID }
3. Resolvers — Python with Strawberry
import strawberry
from [Link] import Info
from typing import Annotated
from dataclasses import field
@[Link]
class User:
id: [Link]
name: str
email: str
role: str
@[Link]
async def grades(self, info: Info) -> list["Grade"]:
# DataLoader batches N+1 queries automatically
loader = [Link]["grade_loader"]
return await [Link](int([Link]))
@[Link]
class Grade:
subject: str
score: float
@[Link]
class Query:
@[Link]
async def me(self, info: Info) -> User | None:
user_id = [Link]["user_id"]
if not user_id:
return None
db = [Link]["db"]
row = await [Link](
"SELECT id, name, email, role FROM users WHERE id = $1", user_id
)
return User(**row) if row else None
@[Link]
async def users(
self,
info: Info,
first: int = 20,
search: str | None = None,
) -> list[User]:
db = [Link]["db"]
q = "SELECT id, name, email, role FROM users"
params = []
if search:
q += " WHERE name ILIKE $1"
[Link](f"%{search}%")
q += f" LIMIT {first}"
rows = await [Link](q, *params)
return [User(**r) for r in rows]
@[Link]
class Mutation:
@[Link]
async def create_user(self, info: Info, name: str, email: str) -> User:
db = [Link]["db"]
row = await [Link](
"INSERT INTO users(name, email) VALUES($1, $2) RETURNING *",
name, email
)
return User(**row)
schema = [Link](query=Query, mutation=Mutation)
# FastAPI integration
from [Link] import GraphQLRouter
app.include_router(GraphQLRouter(schema, context_getter=get_context), prefix="/graphql")
4. The N+1 Problem and DataLoader
The N+1 problem is GraphQL's most notorious performance pitfall. When resolving a list of N users and
each user fetches their grades, you get 1 query for users + N queries for grades = N+1 total queries.
DataLoader solves this by batching all grade lookups into a single query.
from [Link] import DataLoader
# Without DataLoader: N+1 queries
# 1 query: SELECT * FROM users LIMIT 100
# 100 queries: SELECT * FROM grades WHERE user_id = 1
# SELECT * FROM grades WHERE user_id = 2
# ...
# With DataLoader: 2 queries total
async def batch_load_grades(user_ids: list[int]) -> list[list[Grade]]:
# Called once with ALL user_ids batched together
db = get_db()
rows = await [Link](
"SELECT user_id, subject, score FROM grades WHERE user_id = ANY($1)",
user_ids
)
# Group by user_id
grade_map: dict[int, list] = {uid: [] for uid in user_ids}
for row in rows:
grade_map[row["user_id"]].append(Grade(
subject=row["subject"], score=row["score"]
))
return [grade_map[uid] for uid in user_ids]
# Register DataLoader per request (avoid shared state between requests)
async def get_context():
return {
"db": await get_db_connection(),
"grade_loader": DataLoader(load_fn=batch_load_grades),
"user_id": get_current_user_id(),
}
5. Apollo Federation — Distributed GraphQL
Apollo Federation allows splitting a large GraphQL schema across multiple services (subgraphs). A
gateway combines them into a single supergraph that clients query. Each team owns their subgraph
independently.
# User subgraph (users-service)
extend schema
@link(url: "[Link]
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
type Query {
me: User
user(id: ID!): User
}
# Grades subgraph (grades-service) — extends User from users-service
extend schema
@link(url: "[Link]
import: ["@key", "@external", "@extends"])
type User @key(fields: "id") @extends {
id: ID! @external # owned by users-service
grades: [Grade!]! # added by grades-service
}
type Grade {
subject: String!
score: Float!
}
type Query {
grades(userId: ID!): [Grade!]!
}
# Router config (Apollo Router / Gateway)
# [Link]
supergraph:
listen: [Link]:4000
subgraphs:
users:
routing_url: [Link]
grades:
routing_url: [Link]
classes:
routing_url: [Link]
6. GraphQL Security
• Query depth limiting — prevent deeply nested queries that could cause excessive DB joins
• Query complexity analysis — assign costs to fields; reject queries above a threshold
• Persisted queries — clients send a hash of pre-approved queries; unknown queries rejected
• Rate limiting per user — count query complexity consumed per minute, not just request count
• Disable introspection in production — prevents attackers from discovering your entire schema
• Field-level authorization — check permissions inside resolvers, not just at the HTTP layer
• Input validation — validate all mutation inputs with types and custom validators
# Depth limiting and complexity (using graphql-core)
from graphql import build_schema, parse
from [Link] import validate
from [Link] import NoSchemaIntrospectionCustomRule
MAX_DEPTH = 7
MAX_COMPLEXITY = 100
def check_query(query_str: str):
document = parse(query_str)
# Block introspection in production
if not is_dev:
errors = validate(schema, document, rules=[NoSchemaIntrospectionCustomRule])
if errors: raise GraphQLError("Introspection disabled")
# Depth check
depth = get_query_depth(document)
if depth > MAX_DEPTH:
raise GraphQLError(f"Query depth {depth} exceeds max {MAX_DEPTH}")
# Complexity check
complexity = calculate_complexity(document)
if complexity > MAX_COMPLEXITY:
raise GraphQLError(f"Query complexity {complexity} exceeds max {MAX_COMPLEXITY}")
7. Client-Side — Apollo Client
// React + Apollo Client
import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from "@apollo/client";
const client = new ApolloClient({
uri: "[Link]
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: { keyArgs: ["filter"], merge(existing, incoming) { return incoming; } }
}
}
}
}),
headers: { Authorization: `Bearer ${getToken()}` },
});
// Query hook
const GET_USERS = gql`
query GetUsers($search: String, $first: Int) {
users(search: $search, first: $first) {
edges {
node { id name email role grades { subject score } }
}
pageInfo { hasNextPage endCursor }
totalCount
}
}
`;
function UserList() {
const { data, loading, error, fetchMore } = useQuery(GET_USERS, {
variables: { first: 20 },
});
if (loading) return Loading...;
if (error) return Error: {[Link]};
return (
{[Link](({ node }) => (
{[Link]} — {[Link]}
))}
);
}
// Mutation hook
const CREATE_USER = gql`mutation CreateUser($name: String!, $email: String!) {
createUser(input: { name: $name, email: $email }) {
user { id name }
errors { field message }
}
}`;
function CreateUserForm() {
const [createUser, { loading }] = useMutation(CREATE_USER, {
update(cache, { data: { createUser } }) {
[Link]({ fieldName: "users" }); // invalidate users list cache
}
});
// ...
}
8. REST vs GraphQL — Choosing the Right Tool
Scenario Better choice Reason
Public API for third parties REST Simpler to consume, HTTP caching, no GraphQL knowledge needed
Mobile app with varied screen data GraphQL Fetch only needed fields — saves bandwidth on slow connections
Simple CRUD microservice REST Overhead of GraphQL schema not justified for simple operations
Dashboard aggregating many resources
GraphQL One query replaces 5-10 REST calls
File uploads / downloads REST GraphQL handles files awkwardly; use REST multipart endpoints
Real-time features GraphQL Built-in subscriptions; or combine with WebSockets
Internal microservice comms gRPC Binary protocol, faster, better for machine-to-machine
Large product with many teams GraphQL Federation Each team owns schema slice; unified supergraph for frontend