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.
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
- Simple CRUD APIs with predictable data needs
- Public APIs where HTTP caching is critical
- Teams already experienced with REST conventions
- File uploads and streaming (simpler with REST)
When to Choose GraphQL
- Multiple clients with different data requirements (web, mobile, third-party)
- Complex, deeply nested data relationships
- Rapidly evolving frontends that frequently change what data they need
- Microservice architectures where one API gateway aggregates multiple services
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 PlaygroundMutations: 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.
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:
- Chat applications -- new messages in real time
- Live dashboards -- metric updates as they happen
- Notifications -- alerts pushed to the client instantly
- Collaborative editing -- changes from other users appearing live
- Order tracking -- status changes pushed to the customer
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:
String-- UTF-8 textInt-- 32-bit signed integerFloat-- double-precision floating pointBoolean-- true or falseID-- a unique identifier (serialized as a string)
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:
parent-- the result of the parent resolver (useful for nested fields)args-- the arguments passed to the field in the querycontext-- shared state like database connections, authentication info, and data loadersinfo-- metadata about the query (rarely used directly)
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),
}
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:
- GitHub GraphQL API --
https://api.github.com/graphql(requires a personal access token) - Star Wars API (SWAPI) --
https://swapi-graphql.netlify.app/.netlify/functions/index - Countries API --
https://countries.trevorblades.com/graphql - SpaceX API --
https://spacex-production.up.railway.app/graphql
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