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.