Why GraphQL is worth learning in 2026
After working with REST APIs for years, the first time I built something with GraphQL I felt like I had been carrying unnecessary weight. No more overfetching data you do not need. No more underfetching that forces three sequential requests just to render a single page. No more versioning headaches when your front-end team needs slightly different data shapes.
GraphQL, developed internally at Facebook and open-sourced in 2015, has become a first-class citizen in API development. GitHub, Shopify, Twitter, and thousands of other companies use GraphQL in production. In 2026, understanding GraphQL is expected knowledge for any full-stack developer working with modern web applications.
GraphQL vs REST: understanding the difference
With REST, the server defines the data structure and the client adapts. With GraphQL, the client describes exactly what it needs and the server delivers precisely that. This client-driven approach solves the two biggest pain points of REST: overfetching (receiving more data than needed) and underfetching (requiring multiple round trips to get all the data for one screen).
Imagine you are building a profile page that shows a user's name, their last three posts, and each post's comment count. With REST you might call GET /users/1, GET /users/1/posts?limit=3, and then GET /posts/456/comments/count for each post. With GraphQL, one request gets everything.
Setting up your first GraphQL server
We will build a GraphQL API for a blog application using Node.js and Apollo Server 4, the most popular GraphQL server library for JavaScript.
# Create project and install dependencies
mkdir graphql-blog-api && cd graphql-blog-api
npm init -y
npm install @apollo/server graphql
npm install -D nodemon
# Create entry file
touch index.js// index.js — Complete working GraphQL server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Type definitions (schema)
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
published: Boolean!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
type Query {
users: [User!]!
user(id: ID!): User
posts(published: Boolean): [Post!]!
post(id: ID!): Post
}
type Mutation {
createPost(title: String!, body: String!, authorId: ID!): Post!
publishPost(id: ID!): Post!
deletePost(id: ID!): Boolean!
}
`;
// In-memory data for this tutorial
const users = [
{ id: '1', name: 'Alice Johnson', email: 'alice@example.com' },
{ id: '2', name: 'Bob Smith', email: 'bob@example.com' },
];
let posts = [
{ id: '1', title: 'Getting Started with GraphQL', body: 'GraphQL is amazing...', published: true, authorId: '1' },
{ id: '2', title: 'Advanced Apollo Patterns', body: 'After using Apollo for years...', published: false, authorId: '1' },
{ id: '3', title: 'REST vs GraphQL', body: 'Both have their place...', published: true, authorId: '2' },
];
const comments = [
{ id: '1', text: 'Great article!', authorId: '2', postId: '1' },
{ id: '2', text: 'Very helpful, thanks.', authorId: '1', postId: '3' },
];
// Resolvers — the functions that fetch data
const resolvers = {
Query: {
users: () => users,
user: (_, { id }) => users.find(u => u.id === id),
posts: (_, { published }) => {
if (published === undefined) return posts;
return posts.filter(p => p.published === published);
},
post: (_, { id }) => posts.find(p => p.id === id),
},
Mutation: {
createPost: (_, { title, body, authorId }) => {
const post = { id: String(posts.length + 1), title, body, published: false, authorId };
posts.push(post);
return post;
},
publishPost: (_, { id }) => {
const post = posts.find(p => p.id === id);
if (!post) throw new Error('Post not found');
post.published = true;
return post;
},
deletePost: (_, { id }) => {
const index = posts.findIndex(p => p.id === id);
if (index === -1) return false;
posts.splice(index, 1);
return true;
},
},
// Relationship resolvers
User: {
posts: (parent) => posts.filter(p => p.authorId === parent.id),
},
Post: {
author: (parent) => users.find(u => u.id === parent.authorId),
comments: (parent) => comments.filter(c => c.postId === parent.id),
},
Comment: {
author: (parent) => users.find(u => u.id === parent.authorId),
post: (parent) => posts.find(p => p.id === parent.postId),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`GraphQL server running at ${url}`);
Add "type": "module" to your package.json and run node index.js. Open http://localhost:4000 to access Apollo Sandbox, a built-in GraphQL playground.
Writing your first queries
With the server running, you can explore data with GraphQL queries. The power lies in requesting exactly the fields you need.
# Fetch all published posts with author name only
query GetPublishedPosts {
posts(published: true) {
id
title
author {
name
}
}
}
# Fetch a single user with their posts and comment counts
query GetUserProfile {
user(id: "1") {
name
email
posts {
id
title
published
comments {
id
text
}
}
}
}
# Create a new post
mutation CreatePost {
createPost(
title: "My GraphQL Journey"
body: "I started learning GraphQL and it changed how I build APIs..."
authorId: "2"
) {
id
title
published
author {
name
}
}
}
# Publish the post
mutation PublishPost {
publishPost(id: "4") {
id
title
published
}
}Arguments, variables, and fragments
Hardcoding values in queries is fine for exploration but not for real applications. GraphQL variables let you pass dynamic values cleanly, and fragments let you reuse field selections across multiple queries.
# Using variables (the right way to pass dynamic values)
query GetPost($postId: ID!) {
post(id: $postId) {
...PostFields
comments {
text
author {
name
}
}
}
}
# Fragment — reusable field set
fragment PostFields on Post {
id
title
body
published
author {
name
email
}
}
# Variables are sent separately as JSON:
# { "postId": "1" }
# Query with multiple operations and aliases
query Dashboard {
publishedPosts: posts(published: true) {
id
title
}
draftPosts: posts(published: false) {
id
title
}
}Connecting to a real database
In production you will replace the in-memory arrays with real database calls. The resolver pattern makes this straightforward.
// Using Prisma ORM with PostgreSQL
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const resolvers = {
Query: {
posts: async (_, { published }) => {
return prisma.post.findMany({
where: published !== undefined ? { published } : undefined,
include: { author: true },
orderBy: { createdAt: 'desc' },
});
},
user: async (_, { id }) => {
return prisma.user.findUnique({
where: { id: Number(id) },
});
},
},
Mutation: {
createPost: async (_, { title, body, authorId }) => {
return prisma.post.create({
data: { title, body, authorId: Number(authorId), published: false },
include: { author: true },
});
},
},
};
Comparison: REST vs GraphQL
| Aspect | REST | GraphQL |
|---|---|---|
| Data fetching | Fixed endpoints, fixed response shape | Client requests exactly what it needs |
| Overfetching | Common (extra fields always returned) | Eliminated by design |
| Multiple resources | Multiple round trips | Single request for all related data |
| Versioning | v1, v2, v3 endpoints | Schema evolution, deprecation |
| Caching | HTTP caching is straightforward | Requires Apollo or query caching |
| Learning curve | Lower initial barrier | Steeper but pays dividends quickly |
| Tooling | Postman, curl, OpenAPI | Apollo Studio, GraphiQL, Postman |
Common errors and solutions
Error: "Cannot return null for non-nullable field"
This happens when a resolver returns null or undefined for a field marked as required (with ! in the schema). Check your data source: the field might not exist in your database for that record, or your resolver is not returning the right data shape. If the field can legitimately be null, remove the ! from your type definition.
Error: "Field 'X' doesn't exist on type 'Y'"
Your query is requesting a field that is not defined in the schema. This is actually GraphQL working as intended — it catches field name typos at query validation time. Double-check the field name against the schema, or run an introspection query in Apollo Sandbox to see all available fields.
N+1 query problem
When fetching a list of posts, if each post fetches its author via a separate database query, you end up with N+1 queries (1 for the list, N for each author). The solution is DataLoader, which batches multiple requests into one. Install with npm install dataloader and create a loader per request to batch and cache database lookups.
Error: "Variable '$X' of type 'String' used in position expecting type 'ID!'"
GraphQL types are strict. If your schema defines an argument as ID!, you must pass it as a GraphQL ID variable, not String. Update your variable declaration: query MyQuery($id: ID!) instead of query MyQuery($id: String!).
CORS errors when connecting from a browser
When your GraphQL server and front-end run on different ports, you will hit CORS errors. Configure CORS in Apollo Server: pass { cors: { origin: 'http://localhost:3000' } } to startStandaloneServer. In production, replace with your actual domain.
Additional resources
- GraphQL Official Documentation — The definitive reference for the language
- Apollo Server Documentation — Complete guide to building GraphQL servers
- graphql-js on GitHub — The reference JavaScript implementation
- DataLoader — Solve the N+1 problem in GraphQL resolvers
- How to GraphQL — Free fullstack tutorial covering frontend and backend