12 min read

GraphQL for Beginners: Complete Tutorial with Examples

Everything you need to understand GraphQL from the ground up. Covers queries, mutations, subscriptions, schema design, types, resolvers, and a practical comparison with REST -- all with code examples you can run today.

What Is GraphQL and Why Should You Learn It

GraphQL is a query language for APIs and a runtime for executing those queries against your data. Facebook developed it internally in 2012 and open-sourced it in 2015. Since then it has been adopted by GitHub, Shopify, Twitter, Airbnb, and thousands of other companies for both internal and public-facing APIs.

The core idea is simple: the client decides what data it gets back. Instead of hitting a fixed endpoint that returns a predetermined JSON structure, you write a query that describes the exact shape of the data you need, send it to a single endpoint, and get back exactly that shape -- nothing more, nothing less.

# A GraphQL query
{
  user(id: "42") {
    name
    email
    posts {
      title
      publishedAt
    }
  }
}

This single request returns the user's name, email, and all their post titles with publish dates. In a traditional REST API, you would need at least two requests -- one to GET /users/42 and another to GET /users/42/posts -- and each response would likely include fields you do not need.

That difference matters at scale. Mobile clients on slow networks benefit from fewer round trips. Frontend teams benefit from not having to ask the backend team for a new endpoint every time the UI changes. And backend teams benefit from a single, well-typed API surface instead of an ever-growing collection of endpoints.

Key Insight

GraphQL is not a database query language. Despite the name, it has nothing to do with SQL. It sits between your client and your server, giving the client a structured way to ask for exactly the data it needs. The server can fetch that data from databases, microservices, REST APIs, or any other source.

GraphQL vs REST: A Practical Comparison

REST and GraphQL are both valid approaches to API design, and understanding their trade-offs helps you choose the right one for your project. Here is a side-by-side comparison.

Aspect REST GraphQL
Endpoints Multiple (one per resource) Single endpoint for all operations
Data fetching Server decides the response shape Client decides the response shape
Over-fetching Common (extra fields returned) Eliminated (only requested fields)
Under-fetching Common (multiple requests needed) Eliminated (nested data in one query)
Versioning /api/v1/, /api/v2/ patterns No versioning needed (additive schema)
Caching Built-in HTTP caching (ETags, Cache-Control) Requires client-side cache (Apollo, Relay)
Type system Optional (OpenAPI/Swagger) Built-in (schema is the type system)
Real-time Requires WebSockets or SSE separately Subscriptions are part of the spec
Learning curve Lower (HTTP methods + URLs) Higher (schema, resolvers, query language)
Tooling curl, Postman, browser GraphiQL, Apollo Studio, playground tools

When to Choose REST

When to Choose GraphQL

You can test both REST and GraphQL APIs using the NexTool API Tester -- just set the method to POST, point it at your GraphQL endpoint, and paste your query as the JSON request body.

Core Concepts You Need to Know

Before writing your first query, understand these five building blocks. Every GraphQL API is built on them.

1. Schema

The schema defines the entire API surface. It specifies what types exist, what fields each type has, what queries and mutations are available, and how types relate to each other. Think of it as the contract between the client and the server.

2. Types

GraphQL is strongly typed. Every piece of data has a type -- String, Int, Float, Boolean, ID, or a custom object type you define. Types prevent an entire class of bugs by catching mismatches at query validation time, before any code runs.

3. Queries

Queries are read operations. They fetch data without side effects. You write a query, send it to the server, and get back data in the exact shape you requested.

4. Mutations

Mutations are write operations. They create, update, or delete data. Like queries, they return data -- typically the modified resource, so the client can update its local state without a second request.

5. Resolvers

Resolvers are functions on the server that actually fetch the data for each field in the schema. The schema says "a User has a name and an email." The resolver says "here is how to get the name and email from the database." Resolvers are where your business logic lives.

Writing GraphQL Queries

A GraphQL query looks like a JSON object without the values. You specify the fields you want, and the server fills in the values.

Basic Query

# Fetch a single user
query {
  user(id: "42") {
    name
    email
    avatarUrl
  }
}

# Response
{
  "data": {
    "user": {
      "name": "Alice Chen",
      "email": "alice@example.com",
      "avatarUrl": "https://cdn.example.com/avatars/42.jpg"
    }
  }
}

Notice how the response mirrors the query structure exactly. If you remove avatarUrl from the query, it disappears from the response. No extra fields, no wasted bandwidth.

Nested Queries

One of GraphQL's biggest advantages is fetching related data in a single request.

query {
  user(id: "42") {
    name
    posts(limit: 5) {
      title
      publishedAt
      comments {
        text
        author {
          name
        }
      }
    }
  }
}

This query returns a user, their five most recent posts, each post's comments, and each comment's author name -- all in one round trip. With REST, this would require at least four separate requests.

Query Variables

Hardcoding values in queries is fine for testing but impractical in production. Variables let you parameterize queries.

# Query with variables
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}

# Variables (sent as separate JSON)
{
  "userId": "42"
}

The $userId: ID! syntax declares a required variable of type ID. The exclamation mark means it cannot be null. Your client library handles passing the variables alongside the query.

Aliases and Fragments

When you need the same field with different arguments, use aliases. When you repeat the same set of fields, use fragments.

# Aliases: fetch two users in one query
query {
  alice: user(id: "42") {
    ...UserFields
  }
  bob: user(id: "43") {
    ...UserFields
  }
}

# Fragment: reusable field selection
fragment UserFields on User {
  name
  email
  avatarUrl
  createdAt
}

To experiment with queries interactively, open the NexTool GraphQL Playground and connect it to any public GraphQL endpoint. You get syntax highlighting, auto-completion, and instant response formatting.

Test GraphQL Queries in Your Browser

Write queries, inspect responses, and explore schemas with zero setup.

Open GraphQL Playground

Mutations: Writing Data with GraphQL

Mutations follow the same syntax as queries but represent write operations. By convention, mutation names describe the action being performed.

Creating a Resource

mutation {
  createUser(input: {
    name: "Alice Chen"
    email: "alice@example.com"
    role: EDITOR
  }) {
    id
    name
    email
    createdAt
  }
}

The mutation creates a user and returns the new user's id, name, email, and createdAt fields. You choose which fields to get back, just like a query. This eliminates the need for a follow-up GET request after creating a resource.

Updating a Resource

mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
    email
    updatedAt
  }
}

# Variables
{
  "id": "42",
  "input": {
    "name": "Alice Smith",
    "role": "ADMIN"
  }
}

Deleting a Resource

mutation {
  deleteUser(id: "42") {
    success
    message
  }
}

Most GraphQL APIs return a success boolean and an optional message from delete mutations, though the exact return type varies by implementation.

Best Practice

Always return the modified resource from mutations. This lets the client update its cache immediately without a follow-up query. If you are building a mutation that creates a user, return the full user object. If you update a post, return the updated post.

Subscriptions: Real-Time Data

Subscriptions are GraphQL's answer to real-time updates. While queries and mutations follow a request-response pattern, subscriptions open a persistent connection -- typically a WebSocket -- and push updates to the client whenever the specified event occurs.

subscription {
  messageAdded(channelId: "general") {
    id
    text
    author {
      name
      avatarUrl
    }
    createdAt
  }
}

When a new message is added to the "general" channel, the server pushes the message data to every subscribed client. The client specifies exactly which fields it needs, just like with queries and mutations.

Common use cases for subscriptions:

Subscriptions require a WebSocket-capable server. Apollo Server, Hasura, and most modern GraphQL frameworks support them out of the box.

Schema Definition and the Type System

The schema is the backbone of every GraphQL API. It is written in the Schema Definition Language (SDL) and defines every type, field, query, mutation, and subscription your API supports.

Scalar Types

GraphQL includes five built-in scalar types:

You can also define custom scalars like DateTime, URL, or JSON for domain-specific data.

Object Types

type User {
  id: ID!
  name: String!
  email: String!
  bio: String
  role: Role!
  posts: [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  body: String!
  author: User!
  comments: [Comment!]!
  publishedAt: DateTime
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
  createdAt: DateTime!
}

The ! means a field is non-nullable -- the server guarantees it will always have a value. [Post!]! means the field returns a non-null list of non-null Post objects. This type system catches errors before any code runs. If a client queries a field that does not exist, or passes a string where an integer is expected, the server rejects the query at validation time.

Enum Types

enum Role {
  VIEWER
  EDITOR
  ADMIN
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

Enums restrict a field to a fixed set of values. They are self-documenting and prevent invalid states.

Input Types

input CreateUserInput {
  name: String!
  email: String!
  role: Role = VIEWER
}

input UpdateUserInput {
  name: String
  email: String
  role: Role
}

Input types define the shape of data sent to mutations. Note the = VIEWER default value on the role field in CreateUserInput.

The Root Types

type Query {
  user(id: ID!): User
  users(limit: Int = 20, offset: Int = 0): [User!]!
  post(id: ID!): Post
  searchPosts(query: String!): [Post!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: CreatePostInput!): Post!
}

type Subscription {
  messageAdded(channelId: ID!): Message!
  postPublished: Post!
}

These three root types -- Query, Mutation, and Subscription -- are the entry points to your API. Every operation a client can perform starts here.

Use the NexTool JSON Schema Generator to convert sample JSON responses into structured schemas. It is especially useful when you are designing a GraphQL schema based on existing REST API responses.

Resolvers: Where the Data Actually Comes From

A schema declares what data is available. Resolvers implement how that data is fetched. Every field in the schema can have a resolver function. In practice, you only write resolvers for fields that require custom logic -- GraphQL frameworks provide default resolvers for simple property access.

Basic Resolvers in JavaScript

const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      return context.db.users.findById(id);
    },
    users: async (parent, { limit, offset }, context) => {
      return context.db.users.findAll({ limit, offset });
    },
  },

  Mutation: {
    createUser: async (parent, { input }, context) => {
      const user = await context.db.users.create(input);
      return user;
    },
    deleteUser: async (parent, { id }, context) => {
      await context.db.users.delete(id);
      return true;
    },
  },

  User: {
    posts: async (user, args, context) => {
      return context.db.posts.findByAuthorId(user.id);
    },
  },
};

Each resolver receives four arguments:

The N+1 Problem and DataLoader

Consider a query that fetches 20 users and each user's posts. Without optimization, the server runs 1 query for the user list + 20 queries for each user's posts = 21 database queries. This is the N+1 problem.

The standard solution is DataLoader, a batching utility. Instead of fetching posts for each user individually, DataLoader collects all the user IDs, waits for the current execution tick to complete, then runs a single batch query: SELECT * FROM posts WHERE author_id IN (id1, id2, ... id20). This reduces 21 queries to 2.

const DataLoader = require('dataloader');

const postLoader = new DataLoader(async (userIds) => {
  const posts = await db.posts.findByAuthorIds(userIds);
  // Group posts by author_id to match the input order
  return userIds.map(id =>
    posts.filter(post => post.authorId === id)
  );
});

// In the User resolver
User: {
  posts: (user) => postLoader.load(user.id),
}
Performance Tip

Always use DataLoader for resolvers that fetch related data. Without it, a single GraphQL query can generate hundreds of database queries. DataLoader is not optional in production -- it is essential.

Getting Started: Your First GraphQL Server

The fastest way to build a GraphQL server in 2026 is with Apollo Server and Node.js. Here is a minimal working example.

Step 1: Initialize the Project

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

Step 2: Write the Server

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

// Schema
const typeDefs = `
  type Book {
    id: ID!
    title: String!
    author: String!
    year: Int
  }

  type Query {
    books: [Book!]!
    book(id: ID!): Book
  }

  type Mutation {
    addBook(title: String!, author: String!, year: Int): Book!
  }
`;

// In-memory data
let books = [
  { id: "1", title: "The Pragmatic Programmer", author: "David Thomas", year: 1999 },
  { id: "2", title: "Clean Code", author: "Robert C. Martin", year: 2008 },
  { id: "3", title: "Designing Data-Intensive Applications", author: "Martin Kleppmann", year: 2017 },
];

// Resolvers
const resolvers = {
  Query: {
    books: () => books,
    book: (_, { id }) => books.find(b => b.id === id),
  },
  Mutation: {
    addBook: (_, { title, author, year }) => {
      const book = { id: String(books.length + 1), title, author, year };
      books.push(book);
      return book;
    },
  },
};

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

Step 3: Run and Test

# Start the server
node index.js

# Test with curl
curl -X POST http://localhost:4000/ \
  -H "Content-Type: application/json" \
  -d '{"query": "{ books { title author year } }"}'

Open http://localhost:4000 in your browser and Apollo Server serves a built-in GraphQL sandbox where you can write and run queries interactively.

For formatting and inspecting the JSON responses from your GraphQL server, paste the output into the NexTool JSON Formatter for a collapsible, syntax-highlighted tree view.

Practice with Public APIs

If you want to practice queries without setting up a server, these public GraphQL APIs are available:

Frequently Asked Questions

What is GraphQL and how is it different from REST?

GraphQL is a query language for APIs that lets clients request exactly the data they need in a single request. Unlike REST, where each endpoint returns a fixed data structure and you often need multiple requests to assemble a complete view, GraphQL exposes a single endpoint and lets the client specify the shape of the response. This eliminates over-fetching (getting fields you do not need) and under-fetching (needing extra requests for related data). REST organizes data around resources with separate URLs, while GraphQL organizes data around a typed schema that clients query with a flexible syntax.

What are queries, mutations, and subscriptions in GraphQL?

Queries are read operations that fetch data from the server without changing anything. Mutations are write operations that create, update, or delete data on the server and return the modified result. Subscriptions are long-lived connections, typically over WebSockets, that push real-time updates from the server to the client whenever specified data changes. Queries are analogous to GET requests in REST, mutations map to POST, PUT, PATCH, and DELETE, and subscriptions have no direct REST equivalent because REST is inherently request-response based.

What is a GraphQL schema and why does it matter?

A GraphQL schema is a strongly typed contract that defines every type of data your API can return, every query clients can run, every mutation they can perform, and the relationships between types. It is written in the Schema Definition Language (SDL) and serves as the single source of truth for both frontend and backend teams. The schema matters because it enables auto-completion in development tools, automatic documentation generation, compile-time validation of queries, and clear communication between teams about what the API can and cannot do.

Is GraphQL better than REST?

Neither is universally better. GraphQL is a strong choice when your client needs flexible data fetching, when you have multiple clients (web, mobile, third-party) with different data needs, or when you want to reduce the number of network requests. REST is a strong choice for simple CRUD APIs, when you need aggressive HTTP caching, when your team is already experienced with REST conventions, or when your API serves a single client with predictable data needs. Many production systems use both: REST for simple public endpoints and GraphQL for complex internal data fetching.

What tools do I need to start learning GraphQL?

To start learning GraphQL you need a GraphQL playground for writing and testing queries interactively, a JSON formatter for inspecting responses, and optionally an API testing tool for sending raw HTTP requests to a GraphQL endpoint. On the server side, popular frameworks include Apollo Server for JavaScript and TypeScript, Strawberry for Python, and graphql-java for Java. Public GraphQL APIs like the GitHub GraphQL API and the Star Wars API (SWAPI GraphQL) let you practice queries without setting up a server.

Explore 150+ Free Developer Tools

GraphQL Playground, API Tester, JSON Formatter, and more. All browser-based, no signup required.

Browse All Free Tools
NT

NexTool Team

We build free, privacy-first developer tools. Our mission is to make the tools you reach for every day faster, cleaner, and more respectful of your data.