GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. Created by Facebook in 2012 and open-sourced in 2015, GraphQL lets clients request exactly the data they need — nothing more, nothing less. In a single request you can fetch data from multiple resources that would require 3-5 REST calls. Apps using GraphQL see up to 66% better network performance compared to equivalent REST implementations, and enterprise adoption has grown over 340% since 2023. The latest specification is the September 2025 edition, maintained by the GraphQL Foundation under the Linux Foundation.
What Is GraphQL?
GraphQL is a strongly-typed API layer that sits between your clients and your data sources. Unlike REST, where the server defines fixed response shapes, GraphQL lets the client describe what data it needs using a declarative query language. The server then returns a JSON response matching exactly that shape.
Client Server
│ │
│ POST /graphql │
│ { query: "{ user(id: 1) { name } }" } │
│ ──────────────────────────────────────► │
│ │ ← resolves user(id: 1)
│ { "data": { "user": { "name": "Ada" }}}│
│ ◄────────────────────────────────────── │
│ │
✓ Client gets exactly what it asked forEvery GraphQL API exposes a single endpoint (typically /graphql). Clients send queries via HTTP POST and receive JSON responses. The server's schema defines the complete set of available types and operations, and the server validates every query against it before execution.
How Does GraphQL Compare to REST?
| Aspect | REST | GraphQL |
|---|---|---|
| Endpoints | Multiple (one per resource) | Single /graphql endpoint |
| Data fetching | Fixed response shape, over/under-fetching | Client specifies exact fields |
| Versioning | URL versioning (/v1/, /v2/) | Schema evolution, no versioning needed |
| Type system | Optional (OpenAPI/Swagger) | Built-in, required |
| Real-time | Requires separate WebSocket setup | Subscriptions built into spec |
| Caching | HTTP caching works natively | Requires client-side cache (e.g. Apollo) |
| Tooling | Postman, curl, Swagger UI | GraphiQL, Apollo Studio, Playground |
| Learning curve | Lower — uses familiar HTTP verbs | Higher — new query language + schema design |
When to use GraphQL: mobile apps with bandwidth constraints, dashboards aggregating multiple data sources, rapidly evolving frontends, and microservice architectures needing a unified API gateway. When to stick with REST: simple CRUD APIs, file uploads/downloads, public APIs where HTTP caching is critical, and teams already invested in OpenAPI tooling.
How Does the GraphQL Type System Work?
Every GraphQL API is defined by a schema written in the Schema Definition Language (SDL). The schema is a contract between client and server — it defines every type, field, and relationship available in the API.
# Scalar types: Int, Float, String, Boolean, ID (built-in)
# Custom scalars
scalar DateTime
# Enums — restricted to specific values
enum Role {
ADMIN
USER
MODERATOR
}
# Object types — the core building block
type User {
id: ID! # ! means non-nullable
name: String!
email: String!
role: Role!
posts: [Post!]! # non-nullable list of non-nullable Posts
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
tags: [String!]!
published: Boolean!
}
# Input types — used for mutations
input CreatePostInput {
title: String!
body: String!
tags: [String!]
}
# Interface — abstract type with shared fields
interface Node {
id: ID!
}
# Union — one of several types
union SearchResult = User | PostType Modifiers at a Glance
| Syntax | Nullable? | Example value |
|---|---|---|
| String | Field can be null | null or "hello" |
| String! | Field is never null | "hello" |
| [String] | List and items can be null | null, [], ["a", null] |
| [String!] | Items never null, list can be null | null, [], ["a", "b"] |
| [String!]! | Nothing is null | [], ["a", "b"] |
How Do You Write GraphQL Queries?
Queries are how you read data. They mirror the shape of the response — what you write is what you get.
query GetUser {
user(id: "42") {
name
email
posts {
title
published
}
}
}{
"data": {
"user": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"posts": [
{ "title": "First Program", "published": true },
{ "title": "Draft Ideas", "published": false }
]
}
}
}Variables, Aliases, and Fragments
Real-world queries use variables for dynamic values, aliases to rename fields, and fragments to reuse field selections.
# Fragment — reusable field selection
fragment UserBasic on User {
id
name
email
}
# Query with variables and aliases
query GetUsers($firstId: ID!, $secondId: ID!) {
alice: user(id: $firstId) {
...UserBasic
role
}
bob: user(id: $secondId) {
...UserBasic
posts {
title
}
}
}
# Variables (sent as JSON alongside the query)
# { "firstId": "1", "secondId": "2" }Directives
Directives modify query execution. Two are built-in: @include(if: Boolean!) and @skip(if: Boolean!).
query GetUser($id: ID!, $withPosts: Boolean!) {
user(id: $id) {
name
email
posts @include(if: $withPosts) {
title
}
}
}How Do Mutations Work?
Mutations are for writing data — creating, updating, and deleting. Unlike queries which run in parallel, mutation fields execute sequentially (left to right, top to bottom) to avoid race conditions. Best practice: use a single input argument and return the mutated object so the client can update its cache.
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
body
author {
name
}
}
}
# Variables
# {
# "input": {
# "title": "GraphQL in Production",
# "body": "Here's what we learned...",
# "tags": ["graphql", "api"]
# }
# }mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
updatePost(id: $id, input: $input) {
id
title
body
}
}
mutation DeletePost($id: ID!) {
deletePost(id: $id) {
id # return the deleted ID for cache eviction
}
}What Are GraphQL Subscriptions?
Subscriptions provide real-time, push-based updates from server to client over a persistent connection (typically WebSocket). The client subscribes to an event, and the server pushes data whenever that event occurs.
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
# The server pushes a message each time a new post is created:
# { "data": { "postCreated": { "id": "99", "title": "New post!", "author": { "name": "Ada" } } } }Queries
Read data on demand. Executed in parallel. The most common operation type.
query { }Mutations
Write data (create, update, delete). Executed sequentially to prevent race conditions.
mutation { }Subscriptions
Real-time push via WebSocket. Server sends data when events occur.
subscription { }How Do You Build a GraphQL Server?
A GraphQL server needs a schema (types + operations) and resolvers (functions that fetch the actual data). Here is a complete Node.js example using Apollo Server — the most popular GraphQL server library with over 13 million weekly npm downloads.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// 1. Define your schema
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
post(id: ID!): Post
}
input CreatePostInput {
title: String!
body: String!
authorId: ID!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}
`;
// 2. Define resolvers — one function per field
const resolvers = {
Query: {
users: () => db.users.findAll(),
user: (_, { id }) => db.users.findById(id),
post: (_, { id }) => db.posts.findById(id),
},
Mutation: {
createPost: (_, { input }) => db.posts.create(input),
},
// Nested resolvers — called when a field is requested
User: {
posts: (user) => db.posts.findByAuthorId(user.id),
},
Post: {
author: (post) => db.users.findById(post.authorId),
},
};
// 3. Create and start the server
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`GraphQL server ready at ${url}`);Resolver Execution
Every resolver receives four arguments. Understanding them is key to building effective GraphQL APIs.
| Argument | Name | Purpose |
|---|---|---|
| 1st | parent | The result returned by the parent resolver (root resolvers receive undefined) |
| 2nd | args | The arguments passed to this field (e.g. { id: "42" }) |
| 3rd | context | Shared per-request state — auth info, database connections, dataloaders |
| 4th | info | AST and schema metadata about the query — rarely used directly |
How Do You Solve the N+1 Problem with DataLoader?
The N+1 problem is GraphQL's most common performance pitfall. If you query 50 posts and each post resolves its author, that's 1 query for posts + 50 queries for authors = 51 database calls. DataLoader solves this by batching and caching within a single request.
import DataLoader from 'dataloader';
// Create a loader that batches user lookups
// Instead of 50 individual queries, DataLoader makes ONE:
// SELECT * FROM users WHERE id IN (1, 2, 3, ..., 50)
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
const users = await db.users.findByIds([...userIds]);
// IMPORTANT: return results in the same order as the input IDs
return userIds.map(id => users.find(u => u.id === id));
});
// Use in resolvers
const resolvers = {
Post: {
// Without DataLoader: N+1 queries
// author: (post) => db.users.findById(post.authorId),
// With DataLoader: batched into 1 query
author: (post) => userLoader.load(post.authorId),
},
};Without DataLoader (N+1): With DataLoader (batched):
───────────────────────── ──────────────────────────
SELECT * FROM posts; SELECT * FROM posts;
SELECT * FROM users WHERE id=1; SELECT * FROM users
SELECT * FROM users WHERE id=2; WHERE id IN (1,2,3,...,50);
SELECT * FROM users WHERE id=3;
... Total: 2 queries ✓
SELECT * FROM users WHERE id=50;
Total: 51 queries ✗How Do You Query GraphQL from the Client?
You can query any GraphQL API with a plain fetch call. For production apps, a dedicated client like Apollo Client or urql adds caching, optimistic updates, and automatic re-rendering.
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetUser($id: ID!) {
user(id: $id) { name email }
}
`,
variables: { id: '42' },
}),
});
const { data, errors } = await response.json();import { useQuery, gql } from '@apollo/client';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts { title }
}
}
`;
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useQuery(GET_USER, {
variables: { id: userId },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
<ul>
{data.user.posts.map(post => (
<li key={post.title}>{post.title}</li>
))}
</ul>
</div>
);
}What Are GraphQL Best Practices?
Use Input Types for Mutations
Wrap mutation arguments in a single input object type. Keeps the API consistent and simplifies client code.
Return Mutated Objects
Always return the created/updated object from mutations so clients can update their cache without an extra query.
Cursor-Based Pagination
Use connections pattern (edges/nodes/pageInfo) instead of offset pagination for stable, performant pagination.
Schema-First Design
Design your schema before writing resolvers. The schema is your API contract — treat it like a public interface.
Deprecate, Don't Remove
Use @deprecated directive to phase out fields. GraphQL APIs are versionless — evolve the schema instead.
Batch with DataLoader
Always use DataLoader (or equivalent) for nested resolvers to prevent N+1 queries. Create loaders per-request.
Limit Query Depth
Set max depth (typically 7-10) and complexity limits to prevent abuse from deeply nested or expensive queries.
Persisted Queries
In production, send a query hash instead of the full query string. Reduces bandwidth and prevents arbitrary queries.
How Do You Paginate in GraphQL?
The Relay Connection specification is the standard pattern for cursor-based pagination. It provides stable pagination that works with real-time data and avoids the skipped/ duplicated items problem of offset pagination.
type Query {
posts(first: Int, after: String, last: Int, before: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}query GetPosts($cursor: String) {
posts(first: 10, after: $cursor) {
edges {
node {
id
title
author { name }
}
cursor
}
pageInfo {
hasNextPage
endCursor # pass this as $cursor for the next page
}
totalCount
}
}How Do You Handle Errors in GraphQL?
GraphQL always returns HTTP 200 — even when things go wrong. Errors are reported inside the response body. There are two approaches to error handling: the built-in errors array and union-based result types.
{
"data": { "user": null },
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"http": { "status": 404 }
}
}
]
}union CreatePostResult = Post | ValidationError | AuthError
type ValidationError {
field: String!
message: String!
}
type AuthError {
message: String!
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostResult!
}
# Client query — handle each case explicitly
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
... on Post {
id
title
}
... on ValidationError {
field
message
}
... on AuthError {
message
}
}
}What Is Introspection?
GraphQL APIs are self-documenting. Any client can query the schema itself using introspection queries. This powers tools like GraphiQL, Apollo Studio, and code generators.
# List all types in the schema
{
__schema {
types {
name
kind
description
}
}
}
# Get fields for a specific type
{
__type(name: "User") {
name
fields {
name
type {
name
kind
}
}
}
}Security note: disable introspection in production. It exposes your entire API surface. All major server libraries support this — in Apollo Server, set introspection: false.
GraphQL Security Checklist
| Threat | Mitigation | Implementation |
|---|---|---|
| Deeply nested queries | Query depth limiting | Set maxDepth: 10 in validation rules |
| Expensive queries | Query complexity analysis | Assign cost per field, reject above threshold |
| Schema exposure | Disable introspection | introspection: false in production |
| Arbitrary queries | Persisted queries / allowlist | Only accept pre-registered query hashes |
| Rate limiting | Per-operation rate limits | Track cost per query, not just requests |
| Injection attacks | Input validation | Validate at resolver level + use parameterized DB queries |
GraphQL Ecosystem and Tooling
Apollo Server
The most popular Node.js GraphQL server. Production-ready with plugins, caching, and federation.
Apollo Client
Full-featured React/JS client with normalized caching, optimistic UI, and subscription support.
GraphQL Yoga
Lightweight, spec-compliant server by The Guild. Built on standard Fetch API, runs anywhere.
urql
Minimal, extensible GraphQL client. Smaller bundle than Apollo, great for simpler use cases.
GraphQL Code Generator
Generates TypeScript types, React hooks, and resolvers from your schema. Eliminates runtime type errors.
Relay
Meta's GraphQL client for React. Compiler-driven with aggressive optimizations. Steeper learning curve.
Hasura / PostGraphile
Auto-generate a GraphQL API from your PostgreSQL database. Instant CRUD, subscriptions, and auth.
Apollo Federation
Compose multiple GraphQL services into one unified supergraph. The standard for microservice GraphQL.
Frequently Asked Questions
Is GraphQL a database query language?
No. GraphQL is an API query language — it sits between clients and your backend. It is database-agnostic and works with any data source: SQL databases, NoSQL stores, REST APIs, microservices, or even static files. The name is misleading; it has nothing to do with graph databases.
Does GraphQL replace REST?
Not necessarily. GraphQL and REST solve different problems. GraphQL excels when clients need flexible data fetching, multiple resources in one request, or real-time subscriptions. REST is simpler for basic CRUD, benefits from native HTTP caching, and has broader tooling support. Many organizations run both side by side.
How do you handle authentication in GraphQL?
Authentication is typically handled outside GraphQL — in middleware or the HTTP layer. The authenticated user is passed to resolvers via the context object. Authorization (who can access what) is enforced inside resolvers or with schema directives. Never put auth logic in the schema definition itself.
What is the N+1 problem in GraphQL?
The N+1 problem occurs when resolving nested fields triggers one database query per parent item. For example, fetching 50 posts with their authors results in 1 query for posts + 50 queries for authors. The solution is DataLoader, which batches individual lookups into a single query per request cycle.
Can GraphQL handle file uploads?
The GraphQL specification does not cover file uploads. The most common approach is the GraphQL multipart request specification, implemented by libraries like graphql-upload. Alternatively, many teams use a separate REST endpoint or pre-signed URLs for uploads and pass the resulting URL to a GraphQL mutation.
Is GraphQL slower than REST?
Not inherently. A well-optimized GraphQL API with DataLoader and persisted queries performs comparably to REST. GraphQL can actually be faster for mobile clients because it eliminates round trips and over-fetching. However, naive implementations without batching or complexity limits can be significantly slower due to the N+1 problem.
References
- GraphQL Official Learning Guide — comprehensive tutorial covering queries, mutations, schemas, and best practices
- GraphQL Specification (September 2025) — the latest official language specification
- GraphQL Specification GitHub Repository — open-source specification with 14.8k stars, community discussions, and working drafts
- Apollo GraphQL Documentation — docs for Apollo Server, Apollo Client, Federation, and the GraphOS platform
- GraphQL Schema and Type System — official reference for SDL, scalar types, interfaces, unions, and input types
- GraphQL Queries and Mutations — official guide to query syntax, variables, fragments, and directives