0% found this document useful (0 votes)
2 views8 pages

57 GraphQL API Patterns

The document provides an overview of GraphQL, including its fundamental concepts, schema design, resolvers, and performance considerations such as the N+1 problem. It also discusses Apollo Federation for distributed GraphQL architectures, security practices, and client-side implementation using Apollo Client. Additionally, it compares REST and GraphQL to help determine the appropriate use case for each technology.

Uploaded by

romanovskijv508
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views8 pages

57 GraphQL API Patterns

The document provides an overview of GraphQL, including its fundamental concepts, schema design, resolvers, and performance considerations such as the N+1 problem. It also discusses Apollo Federation for distributed GraphQL architectures, security practices, and client-side implementation using Apollo Client. Additionally, it compares REST and GraphQL to help determine the appropriate use case for each technology.

Uploaded by

romanovskijv508
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

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

You might also like