Skip to content

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