GraphQL¶
GraphQL lets the frontend ask for exactly what it needs — no more over-fetching 200KB of JSON just to get a user's name , no more chaining 5 REST calls to render a single page because the API wasn't designed for that view The tradeoff: you push complexity from the frontend to the backend. Your resolvers need to be efficient , your query depth needs limits , and your auth logic needs to live at the resolver level because there's no "this endpoint = this permission" mapping anymore
What GraphQL Actually Is¶
A query language for your API , and a runtime that executes those queries against your data The client sends a query that describes exactly what data it wants , and the server returns exactly that — nothing more , nothing less
A query:
query {
user(id: "123") {
name
email
posts {
title
createdAt
}
}
}
The response:
{
"data": {
"user": {
"name": "Mahmoud",
"email": "mahmoud@example.com",
"posts": [
{ "title": "DevOps 101", "createdAt": "2026-01-15" },
{ "title": "Container Security", "createdAt": "2026-02-20" }
]
}
}
}
Apollo Server on Node.js¶
npm install @apollo/server graphql
const { ApolloServer } = require('@apollo/server')
const { startStandaloneServer } = require('@apollo/server/standalone')
// Schema — define types and operations
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: String!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
`
// Resolvers — functions that fetch the data
const resolvers = {
Query: {
user: async (_, { id }) => {
const result = await db.query('SELECT * FROM users WHERE id = $1', [id])
return result.rows[0]
},
users: async () => {
const result = await db.query('SELECT * FROM users')
return result.rows
},
posts: async () => {
const result = await db.query('SELECT * FROM posts ORDER BY created_at DESC')
return result.rows
}
},
Mutation: {
createUser: async (_, { name, email }) => {
const result = await db.query(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
[name, email]
)
return result.rows[0]
}
},
// Resolve relations between types
User: {
posts: async (parent) => {
const result = await db.query(
'SELECT * FROM posts WHERE author_id = $1',
[parent.id]
)
return result.rows
}
},
Post: {
author: async (parent) => {
const result = await db.query(
'SELECT * FROM users WHERE id = $1',
[parent.author_id]
)
return result.rows[0]
}
}
}
const server = new ApolloServer({ typeDefs, resolvers })
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
})
console.log(`GraphQL ready at ${url}`)
Query , Mutation , Subscription¶
Query — reading data (GET equivalent):
query GetUserProfile {
user(id: "123") {
name
email
bio
avatarUrl
}
}
Mutation — writing data (POST/PUT/DELETE equivalent):
mutation CreateNewPost {
createPost(
title: "GraphQL Deep Dive",
content: "Long article content here...",
authorId: "123"
) {
id
title
createdAt
}
}
Subscription — real-time updates (WebSocket):
subscription OnNewPost {
postCreated {
id
title
author {
name
}
}
}
GraphQL vs REST — When Each Wins¶
Use REST when: * Simple CRUD with predictable data shapes * File uploads are core to the API * Caching is critical (HTTP caching is dead simple with REST) * You have public API consumers who need predictable endpoints * Your team knows REST and doesn't need GraphQL complexity
Use GraphQL when: * Frontend needs different data shapes for different views * Multiple data sources (databases , microservices , third-party APIs) * Mobile apps where bandwidth matters (over-fetching kills performance) * Rapid frontend iteration without backend changes * API gateway pattern — GraphQL as the single entry point
Security — Don't Let Clients Run Wild¶
Query depth limiting — stop nested queries that kill your DB:
const { ApolloServer } = require('@apollo/server')
const depthLimit = require('graphql-depth-limit')
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5) // Max 5 levels of nesting
]
})
Query cost analysis — rate limit by complexity:
const { ApolloServer } = require('@apollo/server')
const { createComplexityLimitRule } = require('graphql-validation-complexity')
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000) // Max complexity score
]
})
Rate limiting:
// GraphQL rate limiting middleware
const rateLimit = require('express-rate-limit')
app.use('/graphql', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
message: { error: 'Too many GraphQL queries, slow down' }
}))
Auth at the resolver level:
const resolvers = {
Query: {
user: async (_, { id }, context) => {
// context.user is set by auth middleware
if (!context.user) {
throw new AuthenticationError('You must be logged in')
}
// Users can only access their own profile
if (context.user.id !== id && !context.user.isAdmin) {
throw new ForbiddenError('You can only view your own profile')
}
const result = await db.query('SELECT * FROM users WHERE id = $1', [id])
return result.rows[0]
}
}
}
The N+1 Problem — GraphQL's Hidden Killer¶
Every resolver that fetches related data runs a separate query. List 100 posts with authors and that's 1 query for posts + 100 queries for authors = 101 queries to the database
Solution — DataLoader:
const DataLoader = require('dataloader')
// Batch function — loads all requested authors in one query
const createAuthorLoader = () => new DataLoader(async (ids) => {
const result = await db.query(
'SELECT * FROM users WHERE id = ANY($1)',
[ids]
)
// DataLoader expects results in the same order as the ids
const userMap = new Map(result.rows.map(user => [user.id, user]))
return ids.map(id => userMap.get(id))
})
// Pass loader through context
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
authorLoader: createAuthorLoader()
})
})
// Use in resolver
const resolvers = {
Post: {
author: async (parent, _, context) => {
return context.authorLoader.load(parent.author_id)
// One batch query instead of N individual queries
}
}
}
next → upcoming_02_websockets.md