GraphQL Introduction: Learn Queries, Mutations, and Your First API Step-by-Step
11 min read
GraphQL typically uses one endpoint that can fetch many related resources in a single request.
Last updated: February 2026 ✅
GraphQL is a modern way to request data from an API — especially when your app needs exactly the right fields, no more and no less.
If REST sometimes feels like:
- “Why am I receiving 20 fields when I only need 3?”
- “Why do I need 3 different endpoints to build one screen?”
- “Why is the frontend waiting on multiple requests?”
GraphQL is designed to solve those problems with one big idea:
✅ The client asks for exactly what it needs.
This tutorial is beginner-first and practical.
You’ll learn GraphQL by building a small API and actually using it.
✅ Key Takeaways
🧠 GraphQL = Ask for fields
You request exactly the shape of data you need instead of accepting a fixed response.
🧾 Schema = contract
Types define what exists and what you’re allowed to query or mutate.
⚡ Build fast + test fast
You’ll run a local server and test queries in an interactive explorer.
🧭 Quick Navigation
👉 Click to open navigation
- 🟣 What Is GraphQL?
- 🆚 GraphQL vs REST (Simple Comparison)
- 🧰 Tools You’ll Use (Programs + Why)
- 🧩 Core Ideas: Schema, Types, Queries, Mutations
- 🏗️ Build Your First GraphQL API (Node + Apollo)
- 🧪 Test Queries & Mutations (Explorer)
- 🖥️ Use GraphQL from a Frontend (fetch)
- 🧱 Practical Patterns (Pagination, Errors, Validation)
- ⚠️ Common GraphQL Mistakes
- 🧪 Exercises / Mini Projects
- ✅ GraphQL Checklist
- ❓ Mini Quiz
- 📚 Recommended Reading
- 💬 FAQ
🟣 What Is GraphQL?
GraphQL is a query language for APIs and a runtime for executing those queries.
Instead of calling multiple REST endpoints like:
/users/1/users/1/posts/posts/50/comments
GraphQL typically exposes one endpoint, often:
/graphql
Then your client sends a query that describes the data it wants.
The key difference (beginner-friendly)
In REST, the server decides the response shape.
In GraphQL, the client decides the response shape (within the schema rules).
That makes GraphQL feel very “frontend-friendly”, but it’s not only for frontend — it’s great for any client that needs flexible data.
🆚 GraphQL vs REST (Simple Comparison)
Here’s a strong beginner comparison table you can keep as a reference:
| Topic | REST | GraphQL |
|---|---|---|
| Endpoints | Many endpoints | Usually one endpoint (/graphql) |
| Response shape | Fixed by server | Chosen by client (query fields) |
| Over-fetching | Common | Reduced (request only fields) |
| Under-fetching | Common (multiple requests) | Often reduced (one request can include nested data) |
| Caching | Straightforward with URLs | Possible, but different approach |
| Error style | Often HTTP status codes | Often 200 with errors in JSON |
| Learning curve | Simple at first | Requires understanding schema + queries |
✅ Both are useful.
GraphQL is not “better”, it’s “better for certain situations”.
🧰 Tools You’ll Use (Programs + Why)
You can build GraphQL in many languages, but for beginners, a JavaScript setup is very practical.
1) Code editor
- VS Code (easy, extensions, clean workflow)
2) Runtime
- Node.js (to run your server)
3) GraphQL testing UI
You’ll test queries in a GraphQL explorer (interactive UI). Many servers provide this automatically.
You may also use:
- curl (quick tests)
- Postman (possible, but GraphQL Explorer is usually easier)
🧩 Core Ideas: Schema, Types, Queries, Mutations
GraphQL feels easy when you understand the vocabulary.
✅ Schema
The schema is the API contract.
It defines:
- What types exist
- What you can query (read)
- What you can mutate (change)
✅ Type
A type is a “shape” of data.
Example: a Task type could have:
idtitledone
✅ Query
A query reads data.
Example:
- “Give me tasks with id and title.”
✅ Mutation
A mutation changes data.
Example:
- “Create a new task.”
- “Mark a task as done.”
✅ Resolver
A resolver is the function that returns the data for a field.
Think of it like:
- Schema defines what can be asked
- Resolver defines how to answer

🧾 Schema and Types (Your First Mental Model)
This is what a simple schema looks like (we’ll build it soon):
type Task {
id: ID!
title: String!
done: Boolean!
}
type Query {
tasks: [Task!]!
task(id: ID!): Task
}
type Mutation {
createTask(title: String!): Task!
setDone(id: ID!, done: Boolean!): Task!
}
What the symbols mean (beginner explanation)
Taskis a typeQuerylists allowed read operationsMutationlists allowed write operations!means “required / cannot be null”[Task!]!means “an array that always exists, and tasks inside it are not null”
🏗️ Build Your First GraphQL API (Node + Apollo)
We will build a small Tasks GraphQL API.
Features:
- list tasks
- get one task
- create task
- update task done state
We’ll start with an in-memory array (simple), then you can upgrade later.
✅ Step 1: Create a project folder
Create a folder:
graphql-introduction
Open it in VS Code.
✅ Step 2: Initialize Node project
In the terminal:
npm init -y
✅ Step 3: Install Apollo Server + GraphQL
npm install @apollo/server graphql
We’ll also install a Node HTTP helper:
npm install express cors
And a small adapter for Apollo + Express:
npm install @as-integrations/express5
✅ Step 4: Create server.js
Create server.js and paste everything below:
import express from "express";
import cors from "cors";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@as-integrations/express5";
// 1) In-memory data (simple "database")
let tasks = [
{ id: "1", title: "Learn GraphQL basics", done: false },
{ id: "2", title: "Run my first query", done: false }
];
let nextId = 3;
// 2) GraphQL schema (API contract)
const typeDefs = `#graphql
type Task {
id: ID!
title: String!
done: Boolean!
}
type Query {
tasks: [Task!]!
task(id: ID!): Task
}
type Mutation {
createTask(title: String!): Task!
setDone(id: ID!, done: Boolean!): Task!
}
`;
// 3) Resolvers (how we answer schema fields)
const resolvers = {
Query: {
tasks: () => tasks,
task: (_, args) => tasks.find(t => t.id === args.id) || null
},
Mutation: {
createTask: (_, args) => {
const title = args.title.trim();
if (!title) {
throw new Error("Title cannot be empty");
}
const newTask = { id: String(nextId++), title, done: false };
tasks.push(newTask);
return newTask;
},
setDone: (_, args) => {
const task = tasks.find(t => t.id === args.id);
if (!task) {
throw new Error("Task not found");
}
task.done = args.done;
return task;
}
}
};
async function start() {
const app = express();
// Apollo Server instance
const server = new ApolloServer({
typeDefs,
resolvers
});
await server.start();
// Middleware
app.use(cors());
app.use(express.json());
// GraphQL endpoint
app.use("/graphql", expressMiddleware(server));
const PORT = 4000;
app.listen(PORT, () => {
console.log(`✅ GraphQL running at http://localhost:${PORT}/graphql`);
});
}
start();
✅ Step 5: Enable ES Modules (important)
Because we used import, Node needs module mode.
Open package.json and add:
"type": "module"
Example:
{
"name": "graphql-introduction",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@apollo/server": "...",
"@as-integrations/express5": "...",
"cors": "...",
"express": "...",
"graphql": "..."
}
}
✅ Step 6: Run the server
node server.js
You should see:
GraphQL running at http://localhost:4000/graphql
Open that URL in your browser.
You should see a GraphQL explorer UI.
🧪 Test Queries & Mutations (Explorer)

Now you will run your first query.
✅ Query 1: List tasks
Paste this into the explorer:
query {
tasks {
id
title
done
}
}
What you should see (explained)
You’ll get a JSON response like:
- an object with
data - inside it,
tasksarray - each task has the fields you requested
✅ Notice: you only receive the fields you listed.
✅ Query 2: Get one task by ID
query {
task(id: "1") {
id
title
done
}
}
If the task doesn’t exist, it returns null.
✅ Mutation 1: Create a task
mutation {
createTask(title: "Build a mutation") {
id
title
done
}
}
What happened (simple explanation)
- You called a “write” operation
- Apollo ran the resolver for
createTask - The resolver created and returned the new task
✅ Mutation 2: Update done state
mutation {
setDone(id: "1", done: true) {
id
title
done
}
}
Now query tasks again and confirm the change.
🧬 Why GraphQL Feels Different (Response Shape)
In REST, the server decides everything.
In GraphQL, these two queries can return different shapes even though they hit the same endpoint:
Request only titles
query {
tasks {
title
}
}
Request everything
query {
tasks {
id
title
done
}
}
That’s the core benefit: precision.
🖥️ Use GraphQL from a Frontend (fetch)
Now let’s call GraphQL from a basic HTML file.
Create index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>GraphQL Fetch Demo</title>
</head>
<body>
<h1>GraphQL Demo</h1>
<button id="load">Load Tasks</button>
<pre id="out"></pre>
<script>
const out = document.getElementById("out");
async function gqlRequest(query, variables = {}) {
const res = await fetch("http://localhost:4000/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables })
});
return res.json();
}
document.getElementById("load").addEventListener("click", async () => {
const query = `
query {
tasks {
id
title
done
}
}
`;
const data = await gqlRequest(query);
out.textContent = JSON.stringify(data, null, 2);
});
</script>
</body>
</html>
What this code does (clear breakdown)
fetch()sends a POST request to/graphql- Body contains
{ query, variables } - Server returns
{ data }(and sometimes{ errors }) - We print the JSON result
If your browser blocks it, confirm:
- server uses
cors() - you’re calling the correct port
4000
🧱 Practical Patterns (Pagination, Errors, Validation)
GraphQL is powerful, but beginners should learn a few safe rules.
✅ Pattern 1: Use variables (cleaner queries)
Instead of hardcoding IDs in queries, use variables.
query GetTask($id: ID!) {
task(id: $id) {
id
title
done
}
}
Variables:
{ "id": "1" }
Why this is good:
- safer
- reusable
- easier to build frontend code
✅ Pattern 2: Understand GraphQL errors
GraphQL often returns HTTP 200 even when something fails.
Errors appear in an errors array.
Example: setDone(id:"999") may return:
datapartially or nullerrorsdescribing the issue
That’s normal in GraphQL.
✅ Pattern 3: Add input types (cleaner mutations)
As APIs grow, you don’t want 10 arguments per mutation.
You can define an input type:
input CreateTaskInput {
title: String!
}
Then mutation becomes:
createTask(input: CreateTaskInput!): Task!
This is cleaner and scales better.

📊 GraphQL Cheat Sheet Table (Beginner Quick Reference)
| Concept | What it means | Example |
|---|---|---|
| Type | Shape of data | type Task { id: ID! } |
| Query | Read operation | query { tasks { id } } |
| Mutation | Write operation | mutation { createTask(...) { id } } |
| Resolver | Function that returns data | tasks: () => tasks |
| Variables | Dynamic values | $id: ID! |
Non-null ! | Required | title: String! |
List [] | Array | [Task!]! |
Save this table. It helps a lot when you feel lost.
⚠️ Common GraphQL Mistakes
| Mistake | Why it happens | Fix |
|---|---|---|
| Treating GraphQL like REST endpoints | Habit from REST | Think “one endpoint, many queries” |
| Making schema too flexible early | Overengineering | Start simple, expand slowly |
| Not validating inputs | Trusting client | Trim + validate in resolvers |
| Returning confusing errors | Throwing random errors | Use clear error messages and consistent patterns |
| Requesting too many fields | “Just in case” mindset | Request only what the screen needs |
| Forgetting CORS | Frontend blocked | Keep cors() enabled for local dev |
🧪 Exercises / Mini Projects
✅ Exercise 1: Add a deleteTask mutation
Goal: remove a task by ID.
Steps:
- Add schema mutation
deleteTask(id: ID!): Boolean! - Implement resolver to remove item
- Return
trueif deleted,falseif not found
👉 Click here to see the solution
// 1) Add in schema: // type Mutation { deleteTask(id: ID!): Boolean! ... } // 2) Add in resolvers: deleteTask: (_, args) => { const index = tasks.findIndex(t => t.id === args.id); if (index === -1) return false; tasks.splice(index, 1); return true; } ✅ Exercise 2: Filter tasks by done
Goal: query only completed tasks.
Add:
tasks(done: Boolean): [Task!]!
Then resolver checks args.
👉 Click here to see the solution
// Schema: // type Query { tasks(done: Boolean): [Task!]! ... } tasks: (_, args) => { if (args.done === undefined) return tasks; return tasks.filter(t => t.done === args.done); } ✅ Exercise 3: Add a new field createdAt
Goal: each task has a timestamp.
Steps:
- Add
createdAt: String!to Task type - Set it when creating
👉 Click here to see the solution
// Schema: type Task { createdAt: String! ... } createTask: (_, args) => { const title = args.title.trim(); if (!title) throw new Error("Title cannot be empty"); const newTask = { id: String(nextId++), title, done: false, createdAt: new Date().toISOString() }; tasks.push(newTask); return newTask; } ✅ GraphQL Checklist
✅ Click to open the checklist
- Server runs locally and prints the GraphQL URL
- Schema defines
Task,Query, andMutation query { tasks { ... } }works in the explorermutation { createTask(...) }creates a new itemmutation { setDone(...) }updates a task- Errors show meaningful messages (not vague)
- Frontend fetch sends POST with
{ query, variables } - CORS is enabled for browser testing
❓ Mini Quiz
❓ What does the GraphQL schema represent?
A contract that defines available types and allowed operations.
❓ What is the difference between a Query and a Mutation?
Query reads data. Mutation changes data.
❓ What does String! mean in GraphQL?
The field is required and cannot be null.
💬 FAQ
Quick answers to common questions about GraphQL for beginners.
❓ Is GraphQL replacing REST?
No. REST is still widely used. GraphQL is an alternative that can be better for flexible data needs and complex UI screens.
❓ Why does GraphQL often return HTTP 200 even when there’s an error?
Because GraphQL treats the request as successfully processed, then reports field-level errors inside an errors array.
❓ Is GraphQL harder for beginners than REST?
It can be at first because you must learn schema + queries, but it becomes very clean once the mental model clicks.
❓ What should I learn after this GraphQL introduction?
Next: authentication, input validation patterns, pagination, and connecting resolvers to a real database.
❓ Can I use GraphQL from mobile apps?
Yes. Any client that can send HTTP requests can use GraphQL.
📚 Recommended Reading
- REST API Tutorial: Build and Use Your First API Step-by-Step
- GitHub Tutorial for Beginners: How to Use GitHub Step-by-Step
- Git & Version Control Hub: Git, GitHub, Branching & Collaboration
- React Beginner Guide: Learn React Step-by-Step from Scratch
- Developer Tools Hub: Free Online Utilities for Programmers