Toolmingo
Guides21 min read

GraphQL Tutorial for Beginners (2025): Learn GraphQL Step by Step

Complete GraphQL tutorial for beginners. Learn GraphQL queries, mutations, subscriptions, schemas, and build real APIs. Free guide with JavaScript and Node.js examples.

GraphQL is a query language for APIs and a runtime for executing those queries, developed by Facebook in 2012 and open-sourced in 2015. Unlike REST, GraphQL lets clients ask for exactly the data they need — no more, no less. Today it powers APIs at GitHub, Twitter, Shopify, and thousands of companies worldwide.

What you'll learn

Topic What you'll be able to do
Core Concepts Understand what GraphQL is and why it matters
Schema & Types Define your data model with SDL
Queries Fetch exactly the data you need
Mutations Create, update, and delete data
Subscriptions Build real-time features
Resolvers Connect schema to your data layer
Apollo Server Build a GraphQL API with Node.js
Apollo Client Query GraphQL from a React frontend

GraphQL vs REST: The Big Picture

Before diving in, understand what problem GraphQL solves:

Problem with REST GraphQL Solution
Overfetching — endpoint returns too much data Client specifies exactly which fields to return
Underfetching — need multiple requests for related data Single query can fetch nested related data
Multiple endpoints (/users, /posts, /comments) Single /graphql endpoint
Versioning hell (/v1/, /v2/) Evolve schema without versioning
Documentation drifts from reality Schema is self-documenting and introspectable
Different teams define different response shapes Strong type system enforced at runtime
REST approach (3 requests):
GET /users/1          → { id, name, email, bio, createdAt, ... }  ← overfetch
GET /users/1/posts    → [ {id, title, content, ...}, ... ]
GET /posts/5/comments → [ {id, body, authorId, ...}, ... ]

GraphQL approach (1 request):
query {
  user(id: "1") {
    name              ← exactly what you need
    posts {
      title
      comments {
        body
        author { name }
      }
    }
  }
}

Setup

Option 1: GraphQL Playground (no install)

Go to GraphQL Playground online or use any public GraphQL API like the GitHub GraphQL API.

Option 2: Local Node.js setup

mkdir graphql-tutorial && cd graphql-tutorial
npm init -y
npm install @apollo/server graphql

Create index.js:

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// 1. Define your schema
const typeDefs = `
  type Query {
    hello: String
  }
`;

// 2. Define resolvers
const resolvers = {
  Query: {
    hello: () => 'Hello from GraphQL!',
  },
};

// 3. Start server
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`🚀 Server ready at ${url}`);

Run it:

node --experimental-vm-modules index.js
# or add "type": "module" to package.json and run: node index.js

Open http://localhost:4000 — you get GraphQL Sandbox to test queries.

Core Concept: The Schema

Every GraphQL API starts with a schema written in Schema Definition Language (SDL). The schema defines your data types and what operations are available.

Scalar Types

GraphQL Type What it stores
String Text
Int 32-bit integer
Float Decimal number
Boolean true / false
ID Unique identifier (serialized as String)

Defining Object Types

type User {
  id: ID!          # ! = non-null (required)
  name: String!
  email: String!
  age: Int         # no ! = nullable (optional)
  bio: String
  createdAt: String!
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!    # relationship — Post has one User
}

The Root Types

Every GraphQL schema has three special root types:

type Query {        # Read operations (like GET in REST)
  user(id: ID!): User
  users: [User!]!
  post(id: ID!): Post
  posts(published: Boolean): [Post!]!
}

type Mutation {     # Write operations (like POST/PUT/DELETE)
  createUser(name: String!, email: String!): User!
  updateUser(id: ID!, name: String): User!
  deleteUser(id: ID!): Boolean!
  createPost(title: String!, content: String!, authorId: ID!): Post!
}

type Subscription { # Real-time (like WebSockets)
  postAdded: Post!
  userJoined: User!
}

Queries

Queries fetch data. The client specifies exactly which fields it wants.

Basic Query

query {
  users {
    id
    name
    email
  }
}

Response:

{
  "data": {
    "users": [
      { "id": "1", "name": "Alice", "email": "alice@example.com" },
      { "id": "2", "name": "Bob", "email": "bob@example.com" }
    ]
  }
}

Query with Arguments

query {
  user(id: "1") {
    name
    email
    bio
  }
}

Nested Queries (the killer feature)

Fetch user + their posts + each post's comments in one request:

query {
  user(id: "1") {
    name
    posts {
      title
      published
      comments {
        body
        author {
          name
        }
      }
    }
  }
}

Named Queries

Always name your queries — it helps with debugging and caching:

query GetUserProfile {
  user(id: "1") {
    name
    email
  }
}

Query Variables

Hard-coding IDs in queries is bad. Use variables instead:

query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
    posts {
      title
    }
  }
}

Pass variables as JSON alongside the query:

{
  "userId": "1"
}

Aliases

Fetch the same field with different arguments in one query:

query {
  published: posts(published: true) {
    title
  }
  drafts: posts(published: false) {
    title
  }
}

Fragments

Reuse field selections across multiple queries:

fragment UserFields on User {
  id
  name
  email
}

query {
  user(id: "1") {
    ...UserFields
    posts {
      title
    }
  }
  me {
    ...UserFields
  }
}

Mutations

Mutations modify data (create, update, delete).

Basic Mutation

mutation {
  createUser(name: "Charlie", email: "charlie@example.com") {
    id
    name
    email
  }
}

You specify which fields of the returned object you want — same as queries.

Mutation with Variables (correct approach)

mutation CreateUser($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    id
    name
    createdAt
  }
}

Variables:

{
  "name": "Charlie",
  "email": "charlie@example.com"
}

Input Types

For complex mutations, define an input type:

input CreatePostInput {
  title: String!
  content: String!
  authorId: ID!
  published: Boolean = false
}

type Mutation {
  createPost(input: CreatePostInput!): Post!
}

Usage:

mutation {
  createPost(input: {
    title: "GraphQL is Amazing"
    content: "Here's why..."
    authorId: "1"
    published: true
  }) {
    id
    title
    author {
      name
    }
  }
}

Resolvers

Resolvers are functions that fetch data for each field in your schema. They're the "how" behind the "what" of your schema.

Resolver Signature

fieldName(parent, args, context, info) {
  // parent: resolved value of parent type
  // args: arguments passed to this field
  // context: shared across all resolvers (auth, db connection, etc.)
  // info: schema metadata (rarely used)
}

Complete Resolver Example

// In-memory data (swap with database in production)
const users = [
  { id: '1', name: 'Alice', email: 'alice@example.com', age: 30 },
  { id: '2', name: 'Bob', email: 'bob@example.com', age: 25 },
];

const posts = [
  { id: '1', title: 'Hello GraphQL', content: 'GraphQL rocks!', published: true, authorId: '1' },
  { id: '2', title: 'REST vs GraphQL', content: 'Let me compare...', published: false, authorId: '1' },
  { id: '3', title: 'Node.js Tips', content: 'Pro tips...', published: true, authorId: '2' },
];

const resolvers = {
  Query: {
    // List all users
    users: () => users,

    // Get single user by ID
    user: (_, { id }) => users.find(u => u.id === id),

    // List posts, optionally filter by published
    posts: (_, { published }) => {
      if (published !== undefined) {
        return posts.filter(p => p.published === published);
      }
      return posts;
    },

    // Get single post
    post: (_, { id }) => posts.find(p => p.id === id),
  },

  Mutation: {
    createUser: (_, { name, email }) => {
      const user = { id: String(users.length + 1), name, email };
      users.push(user);
      return user;
    },

    createPost: (_, { title, content, authorId }) => {
      const post = {
        id: String(posts.length + 1),
        title,
        content,
        published: false,
        authorId,
      };
      posts.push(post);
      return post;
    },

    deleteUser: (_, { id }) => {
      const index = users.findIndex(u => u.id === id);
      if (index === -1) throw new Error('User not found');
      users.splice(index, 1);
      return true;
    },
  },

  // Type resolvers — how to resolve fields on a type
  User: {
    posts: (parent) => posts.filter(p => p.authorId === parent.id),
  },

  Post: {
    author: (parent) => users.find(u => u.id === parent.authorId),
  },
};

The N+1 Problem and DataLoader

Without DataLoader, fetching 100 posts and their authors = 101 DB queries:

// BAD: N+1 — each post triggers a separate user lookup
Post: {
  author: (parent) => db.users.findById(parent.authorId), // called N times!
}

Fix with DataLoader (batches and caches):

npm install dataloader
import DataLoader from 'dataloader';

// Create loader (batches multiple IDs into one query)
const userLoader = new DataLoader(async (ids) => {
  const users = await db.users.findByIds(ids); // ONE query for all IDs
  return ids.map(id => users.find(u => u.id === id));
});

// Pass via context so resolvers can use it
const context = { userLoader };

// In resolver — batched automatically
Post: {
  author: (parent, _, { userLoader }) => userLoader.load(parent.authorId),
}

Building a Complete API

Full working blog API with Apollo Server:

// server.js
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { GraphQLError } from 'graphql';

// Schema
const typeDefs = `
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    published: Boolean!
    author: User!
    createdAt: String!
  }

  input CreateUserInput {
    name: String!
    email: String!
  }

  input CreatePostInput {
    title: String!
    content: String!
    authorId: ID!
  }

  input UpdatePostInput {
    title: String
    content: String
    published: Boolean
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts(published: Boolean): [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: UpdatePostInput!): Post!
    deletePost(id: ID!): Boolean!
    publishPost(id: ID!): Post!
  }
`;

// Data store
const db = {
  users: [
    { id: '1', name: 'Alice', email: 'alice@example.com' },
    { id: '2', name: 'Bob', email: 'bob@example.com' },
  ],
  posts: [
    { id: '1', title: 'Hello World', content: 'First post!', published: true, authorId: '1', createdAt: new Date().toISOString() },
    { id: '2', title: 'GraphQL Guide', content: 'Deep dive...', published: false, authorId: '1', createdAt: new Date().toISOString() },
  ],
};

// Resolvers
const resolvers = {
  Query: {
    users: () => db.users,
    user: (_, { id }) => {
      const user = db.users.find(u => u.id === id);
      if (!user) throw new GraphQLError('User not found', { extensions: { code: 'NOT_FOUND' } });
      return user;
    },
    posts: (_, { published }) =>
      published !== undefined ? db.posts.filter(p => p.published === published) : db.posts,
    post: (_, { id }) => db.posts.find(p => p.id === id),
  },

  Mutation: {
    createUser: (_, { input }) => {
      if (db.users.find(u => u.email === input.email)) {
        throw new GraphQLError('Email already exists', { extensions: { code: 'BAD_USER_INPUT' } });
      }
      const user = { id: String(db.users.length + 1), ...input };
      db.users.push(user);
      return user;
    },

    createPost: (_, { input }) => {
      const author = db.users.find(u => u.id === input.authorId);
      if (!author) throw new GraphQLError('Author not found', { extensions: { code: 'NOT_FOUND' } });
      const post = {
        id: String(db.posts.length + 1),
        title: input.title,
        content: input.content,
        published: false,
        authorId: input.authorId,
        createdAt: new Date().toISOString(),
      };
      db.posts.push(post);
      return post;
    },

    updatePost: (_, { id, input }) => {
      const index = db.posts.findIndex(p => p.id === id);
      if (index === -1) throw new GraphQLError('Post not found', { extensions: { code: 'NOT_FOUND' } });
      db.posts[index] = { ...db.posts[index], ...input };
      return db.posts[index];
    },

    deletePost: (_, { id }) => {
      const index = db.posts.findIndex(p => p.id === id);
      if (index === -1) throw new GraphQLError('Post not found', { extensions: { code: 'NOT_FOUND' } });
      db.posts.splice(index, 1);
      return true;
    },

    publishPost: (_, { id }) => {
      const post = db.posts.find(p => p.id === id);
      if (!post) throw new GraphQLError('Post not found', { extensions: { code: 'NOT_FOUND' } });
      post.published = true;
      return post;
    },
  },

  User: {
    posts: (parent) => db.posts.filter(p => p.authorId === parent.id),
  },

  Post: {
    author: (parent) => db.users.find(u => u.id === parent.authorId),
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`🚀 Server ready at: ${url}`);

Test with these queries in GraphQL Sandbox:

# Get all users with their posts
query {
  users {
    name
    email
    posts {
      title
      published
    }
  }
}

# Create a user
mutation {
  createUser(input: { name: "Charlie", email: "charlie@example.com" }) {
    id
    name
  }
}

# Create a post
mutation {
  createPost(input: { title: "My Post", content: "Content here", authorId: "1" }) {
    id
    title
    author {
      name
    }
  }
}

# Publish a post
mutation {
  publishPost(id: "2") {
    title
    published
  }
}

Subscriptions (Real-Time)

Subscriptions use WebSockets to push data to clients when events happen.

npm install @apollo/server @graphql-subscriptions ws graphql-ws
import { createServer } from 'http';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';
import express from 'express';

const pubsub = new PubSub();
const POST_ADDED = 'POST_ADDED';

const typeDefs = `
  type Post {
    id: ID!
    title: String!
    content: String!
  }

  type Query {
    posts: [Post!]!
  }

  type Mutation {
    addPost(title: String!, content: String!): Post!
  }

  type Subscription {
    postAdded: Post!
  }
`;

const posts = [];

const resolvers = {
  Query: {
    posts: () => posts,
  },
  Mutation: {
    addPost: (_, { title, content }) => {
      const post = { id: String(posts.length + 1), title, content };
      posts.push(post);
      pubsub.publish(POST_ADDED, { postAdded: post }); // notify subscribers
      return post;
    },
  },
  Subscription: {
    postAdded: {
      subscribe: () => pubsub.asyncIterator([POST_ADDED]),
    },
  },
};

// Subscribe in a client:
// subscription { postAdded { id title content } }

Directives

Directives modify how fields are fetched. Built-in directives:

Directive Usage Effect
@include(if: Boolean) field @include(if: $showDetails) Include field only if true
@skip(if: Boolean) field @skip(if: $isHidden) Skip field if true
@deprecated(reason: String) On field in schema Marks field as deprecated
query GetUser($showEmail: Boolean!) {
  user(id: "1") {
    name
    email @include(if: $showEmail)    # only included if $showEmail = true
    bio @skip(if: $showEmail)         # only included if $showEmail = false
  }
}

Variables:

{ "showEmail": true }

Authentication & Context

Use context to pass auth info to all resolvers:

const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => {
    // Extract and verify token from headers
    const token = req.headers.authorization?.replace('Bearer ', '');
    let currentUser = null;

    if (token) {
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        currentUser = await db.users.findById(decoded.userId);
      } catch {
        // Invalid token — currentUser stays null
      }
    }

    return { currentUser, db };
  },
});

// In resolver — check auth
const resolvers = {
  Mutation: {
    createPost: (_, { input }, { currentUser }) => {
      if (!currentUser) {
        throw new GraphQLError('Not authenticated', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }
      // ... create post
    },
  },
};

Apollo Client (React)

Query a GraphQL API from React:

npm install @apollo/client graphql
// main.jsx
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery, useMutation } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/',
  cache: new InMemoryCache(),
});

// Wrap your app
function App() {
  return (
    <ApolloProvider client={client}>
      <PostList />
    </ApolloProvider>
  );
}

// Query hook
const GET_POSTS = gql`
  query GetPosts {
    posts(published: true) {
      id
      title
      author {
        name
      }
    }
  }
`;

function PostList() {
  const { loading, error, data } = useQuery(GET_POSTS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.posts.map(post => (
        <li key={post.id}>
          <strong>{post.title}</strong> by {post.author.name}
        </li>
      ))}
    </ul>
  );
}

// Mutation hook
const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!, $authorId: ID!) {
    createPost(input: { title: $title, content: $content, authorId: $authorId }) {
      id
      title
    }
  }
`;

function AddPost() {
  const [createPost, { loading, error }] = useMutation(CREATE_POST, {
    refetchQueries: [{ query: GET_POSTS }], // re-fetch list after mutation
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    await createPost({
      variables: { title: 'New Post', content: 'Content', authorId: '1' },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={loading}>Add Post</button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

GraphQL with a Real Database (Prisma)

Production setup with PostgreSQL:

npm install @prisma/client prisma
npx prisma init

prisma/schema.prisma:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        String   @id @default(cuid())
  name      String
  email     String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
}
npx prisma migrate dev --name init

GraphQL resolvers using Prisma:

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

const resolvers = {
  Query: {
    users: () => prisma.user.findMany({ include: { posts: true } }),
    user: (_, { id }) => prisma.user.findUnique({ where: { id } }),
    posts: (_, { published }) =>
      prisma.post.findMany({
        where: published !== undefined ? { published } : {},
        include: { author: true },
        orderBy: { createdAt: 'desc' },
      }),
  },

  Mutation: {
    createUser: (_, { input }) =>
      prisma.user.create({ data: input }),

    createPost: (_, { input }) =>
      prisma.post.create({
        data: {
          title: input.title,
          content: input.content,
          author: { connect: { id: input.authorId } },
        },
        include: { author: true },
      }),

    publishPost: (_, { id }) =>
      prisma.post.update({
        where: { id },
        data: { published: true },
        include: { author: true },
      }),

    deletePost: async (_, { id }) => {
      await prisma.post.delete({ where: { id } });
      return true;
    },
  },

  User: {
    posts: (parent) => prisma.post.findMany({ where: { authorId: parent.id } }),
  },

  Post: {
    author: (parent) => prisma.user.findUnique({ where: { id: parent.authorId } }),
  },
};

3 Projects to Build

Project 1: Countries API Explorer

Query the public Countries GraphQL API (no auth needed):

# Public API: https://countries.trevorblades.com/

query {
  countries(filter: { continent: { eq: "EU" } }) {
    name
    capital
    currency
    languages {
      name
    }
    emoji
  }
}

query {
  country(code: "US") {
    name
    capital
    states {
      name
    }
  }
}

Project 2: Todo API

Full CRUD GraphQL API:

type Todo {
  id: ID!
  text: String!
  completed: Boolean!
  createdAt: String!
}

type Query {
  todos(completed: Boolean): [Todo!]!
  todo(id: ID!): Todo
}

type Mutation {
  addTodo(text: String!): Todo!
  toggleTodo(id: ID!): Todo!
  deleteTodo(id: ID!): Boolean!
  clearCompleted: Int!  # returns count deleted
}

Implement resolvers with in-memory storage, then connect to PostgreSQL via Prisma.

Project 3: Blog API with Auth

Full-featured blog:

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!
  tags: [String!]!
  createdAt: String!
}

type AuthPayload {
  token: String!
  user: User!
}

type Query {
  me: User                          # requires auth
  posts(tag: String, published: Boolean): [Post!]!
  post(id: ID!): Post
}

type Mutation {
  signup(name: String!, email: String!, password: String!): AuthPayload!
  login(email: String!, password: String!): AuthPayload!
  createPost(title: String!, content: String!, tags: [String!]): Post!  # requires auth
  publishPost(id: ID!): Post!       # requires auth, must be owner
  deletePost(id: ID!): Boolean!     # requires auth, must be owner
}

Error Handling

GraphQL always returns HTTP 200 with errors in the errors array:

{
  "data": null,
  "errors": [
    {
      "message": "User not found",
      "extensions": {
        "code": "NOT_FOUND",
        "http": { "status": 404 }
      }
    }
  ]
}

Throw errors in resolvers:

import { GraphQLError } from 'graphql';

// Apollo Server error codes
throw new GraphQLError('Not logged in', {
  extensions: { code: 'UNAUTHENTICATED' },
});

throw new GraphQLError('User not found', {
  extensions: { code: 'NOT_FOUND' },
});

throw new GraphQLError('Invalid input', {
  extensions: {
    code: 'BAD_USER_INPUT',
    field: 'email',
  },
});

throw new GraphQLError('Permission denied', {
  extensions: { code: 'FORBIDDEN' },
});

Pagination

Two common patterns:

Offset Pagination (simple)

type Query {
  posts(limit: Int = 10, offset: Int = 0): PostsResult!
}

type PostsResult {
  posts: [Post!]!
  total: Int!
  hasMore: Boolean!
}
posts: (_, { limit = 10, offset = 0 }) => {
  const paginated = db.posts.slice(offset, offset + limit);
  return {
    posts: paginated,
    total: db.posts.length,
    hasMore: offset + limit < db.posts.length,
  };
},

Cursor Pagination (scalable, works with GraphQL Cursor Connections spec)

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
}

type PostEdge {
  node: Post!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

type Query {
  posts(first: Int, after: String): PostConnection!
}

GraphQL vs REST vs gRPC

Feature REST GraphQL gRPC
Data fetching Multiple endpoints Single endpoint, choose fields Service methods
Overfetching Common Eliminated Eliminated
Underfetching Common Eliminated Rare
Real-time SSE/WebSocket add-on Built-in subscriptions Server streaming
Type system OpenAPI (optional) Schema required Protobuf required
Browser support Native Native (HTTP) Needs proxy
Learning curve Low Medium Higher
Caching HTTP cache (simple) More complex (persisted queries) Application-level
Best for Simple APIs, public APIs Complex data graphs, mobile Internal microservices

When to Use GraphQL

Use GraphQL when:

  • Multiple clients (web, mobile, desktop) need different data shapes
  • Deeply nested relational data (social networks, e-commerce, CMS)
  • Rapid product iteration — frontend can evolve without backend changes
  • You want self-documenting API with introspection
  • Aggregating multiple data sources into one API (BFF pattern)

Stick with REST when:

  • Simple CRUD API with uniform responses
  • Public API (GraphQL introspection can expose too much)
  • File uploads are the core use case
  • Team is more familiar with REST
  • Simple caching is critical (REST has better HTTP cache support)

GraphQL Tooling

Tool Purpose
Apollo Studio GraphQL schema registry + explorer
GraphQL Playground / Sandbox In-browser IDE for testing
Postman Supports GraphQL queries
graphql-codegen Auto-generate TypeScript types from schema
eslint-plugin-graphql Lint GraphQL queries in JS/TS
GraphQL Inspector Schema change detection, breaking change alerts
Pothos Code-first schema builder for TypeScript
TypeGraphQL Decorator-based schema from TypeScript classes
Nexus Code-first schema, similar to Pothos
Hasura Auto-generate GraphQL from PostgreSQL database
Stepzen Generate GraphQL from REST APIs
WunderGraph API gateway with generated type-safe client

Common Mistakes

Mistake Problem Fix
Not using variables Unsafe string interpolation, no caching Always use query Name($var: Type!) + variables
Ignoring N+1 problem 100 users → 101 DB queries Use DataLoader for batch loading
Putting business logic in resolvers Untestable, hard to reuse Extract to service layer
Returning any from resolvers Type safety lost Match resolver return type to schema
No error handling Crashes expose stack traces Wrap with try/catch, throw GraphQLError
Exposing raw database errors Security risk Map internal errors to friendly messages
No authorization in resolvers Data leakage Check permissions in every resolver
Over-nesting the schema Queries become huge Limit depth, use pagination

GraphQL vs Related Terms

Term What it is
GraphQL Query language + runtime specification
Apollo Server Most popular GraphQL server for Node.js
Apollo Client GraphQL client for React/JavaScript
SDL Schema Definition Language — syntax for writing GraphQL schemas
Resolver Function that fetches data for a schema field
Introspection GraphQL's self-documentation feature (__schema)
DataLoader Library for batching and caching DB calls (fixes N+1)
Subscriptions Real-time GraphQL via WebSockets
Federation Combining multiple GraphQL services into one graph
Hasura Tool that auto-generates GraphQL API from a database

Learning Path

Stage Duration Focus
1. REST basics 0-1 weeks HTTP, JSON, GET/POST/PUT/DELETE
2. GraphQL concepts 1-2 weeks Schema, queries, mutations, types
3. Apollo Server 2-3 weeks Build Node.js GraphQL API
4. Resolvers & data 3-4 weeks Database integration, N+1, DataLoader
5. Apollo Client 4-5 weeks Query/Mutation/Subscription hooks in React
6. Auth & security 5-6 weeks JWT context, authorization patterns
7. Production 6-8 weeks Pagination, caching, error handling, Federation

Quick Reference

# Query — fetch data
query GetUser($id: ID!) {
  user(id: $id) {
    name
    posts { title }
  }
}

# Mutation — modify data
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
  }
}

# Subscription — real-time
subscription {
  postAdded {
    id
    title
    author { name }
  }
}

# Fragment — reusable field selection
fragment PostFields on Post {
  id
  title
  published
  author { name }
}

# Directive — conditional field
query ($showEmail: Boolean!) {
  user(id: "1") {
    name
    email @include(if: $showEmail)
  }
}

Frequently Asked Questions

Do I need to know REST before GraphQL?
Yes — understanding HTTP, JSON, and API concepts makes GraphQL much easier to learn. REST first, GraphQL second.

Is GraphQL a database query language?
No. Despite the name, GraphQL is an API query language. It has nothing to do with SQL or databases directly. Resolvers connect GraphQL to any data source (PostgreSQL, MongoDB, REST API, files, etc.).

Does GraphQL replace REST?
Not always. GraphQL and REST can coexist. GraphQL excels with complex data graphs and multiple clients. REST is simpler for basic CRUD APIs and is better understood by HTTP infrastructure (CDN caching, etc.).

How does GraphQL handle caching?
Differently from REST. Since everything is a POST to one endpoint, HTTP caching doesn't work automatically. Solutions: Apollo Client's InMemoryCache (client-side), Persisted Queries (cache by query hash), CDN layer with normalized caching (e.g., Stellate).

What's the difference between Apollo Server and Yoga Server?
Both are popular GraphQL servers for Node.js. Apollo Server is older, widely adopted, battle-tested. GraphQL Yoga (by The Guild) is newer, more lightweight, closer to the spec, and spec-compliant. Both are good choices.

Can I use GraphQL with any database?
Yes. GraphQL is database-agnostic. Resolvers can fetch data from PostgreSQL, MySQL, MongoDB, Redis, REST APIs, files, or anything else. The schema is your API contract — the implementation is up to you.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools