GraphQL is a query language for your API and a runtime for executing those queries by using a type system you define for your data. It's not tied to any specific database or storage engine and is instead backed by your existing code and data.
Getting more data than you need (e.g., a REST endpoint returns a full user object but you only need the username).
Not getting enough data with a single request, forcing you to call multiple endpoints (e.g., fetch a user, then fetch their posts, then fetch their friends).
Frontend developers can request exactly what they need without waiting for backend teams to create new, specific endpoints.
Instead of thinking about isolated endpoints (users, posts), you think about your data as a connected graph. You ask for a user, and you can traverse the graph to get their posts, and then the comments on those posts, all in one request.
Example: GraphQL models data as a graph — nodes (objects) connected by edges (relationships).
{
user(id: "1") {
name
posts {
title
author {
name
}
}
}
}
This represents a graph: User → Posts → Author.
Instead of multiple REST calls:
# GET /api/users/1
# GET /api/users/1/posts
# GET /api/users/1/followers
Single GraphQL query:
query {
user(id: 1) {
name
email
posts {
title
content
}
followers {
name
}
}
}
These are the three main operation types in GraphQL.
Queries are used to read or fetch data. (Equivalent to GET in REST).
query {
users {
id
name
email
}
}
query {
user(id: "1") {
name
email
posts(limit: 5) {
title
createdAt
}
}
}
Fields: name
, email
, title
, createdAt
are fields. You are asking for these specific properties.
Arguments: You can pass arguments to fields to be more specific. id: "1"
is an argument to the user
field. limit: 5
is an argument to the posts
field.
Variables: Instead of hardcoding "1"
, you use variables to make queries dynamic.
query GetUser($userId: ID!) {
user(id: $userId) {
name
}
}
Aliases: If you need to ask for the same field with different arguments, you use aliases to avoid conflicts.
query {
firstPost: post(id: "1") { title }
secondPost: post(id: "2") { title }
}
Fragments: Reusable sets of fields to avoid duplication.
fragment PostDetails on Post {
title
body
author { name }
}
query {
post1: post(id: "1") { ...PostDetails }
post2: post(id: "2") { ...PostDetails }
}
Directives: Dynamically change the structure of your query. @include
and @skip
are common.
query ($withEmail: Boolean!) {
user(id: "1") {
name
email @include(if: $withEmail) # email is only fetched if $withEmail is true
}
}
Mutations are used to write data (create, update, delete). (Equivalent to POST, PUT, PATCH, DELETE in REST).
mutation {
createUser(input: {
name: "John Doe"
email: "john@example.com"
}) {
id
name
email
}
}
mutation {
createPost(input: {
title: "GraphQL Guide"
content: "Learning GraphQL..."
}) {
id
title
}
updateUser(id: "123", input: {
name: "Updated Name"
}) {
id
name
}
}
Subscriptions are used to get real-time updates pushed from the server. They are typically implemented using WebSockets.
subscription {
commentAdded(postId: "123") {
id
content
author {
name
}
createdAt
}
}
The schema is the contract between the client and the server. It defines all the available data and operations.
type Query {
user(id: ID!): User
posts(limit: Int): [Post!]!
}
type Mutation {
createPost(title: String!, body: String!): Post!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
Built-in primitive types: Int
, Float
, String
, Boolean
, ID
(a unique identifier).
Custom Scalars:
scalar DateTime
scalar Email
scalar URL
type User {
email: Email!
website: URL
createdAt: DateTime!
}
The core building blocks, like User
and Post
, which are made up of fields.
Denoted by [ ]
. [Post]
means a list of Post
objects.
type User {
friends: [User!]! # Non-null array of non-null Users
nicknames: [String] # Nullable array of nullable Strings
}
A defined set of allowed values.
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type Post {
status: PostStatus!
}
An abstract type that defines a set of fields. Other types must implement these fields.
interface Notification {
id: ID!
message: String!
}
type EmailNotification implements Notification {
id: ID!
message: String!
email: String!
}
A way to return one of several possible types.
union SearchResult = Photo | Post | User
type Query {
search(text: String!): [SearchResult!]!
}
Functions that fetch the actual data for schema fields.
const resolvers = {
Query: {
user: (parent, args, context, info) => {
return context.db.users.find(u => u.id === args.id);
},
users: (parent, args, context, info) => {
return context.db.users.slice(args.offset, args.offset + args.limit);
}
},
User: {
posts: (user, args, context, info) => {
return context.db.posts.filter(p => p.authorId === user.id);
}
},
Mutation: {
createUser: (parent, args, context, info) => {
const newUser = {
id: generateId(),
...args.input
};
context.db.users.push(newUser);
return newUser;
}
}
};
Standard way to serve GraphQL APIs.
data
, errors
, extensions
Request:
POST /graphql
Content-Type: application/json
{
"query": "{ user(id: \"123\") { name } }",
"variables": {},
"operationName": null
}
Response:
{
"data": {
"user": {
"name": "John Doe"
}
},
"errors": null
}
Securing GraphQL endpoints with JWT, API keys, OAuth.
const resolvers = {
Query: {
sensitiveData: (parent, args, context) => {
if (!context.user || !context.user.isAdmin) {
throw new Error('Unauthorized');
}
return getSensitiveData();
}
}
};
Avoid loading all data at once, improve performance, and provide better user experience.
Using cursors for stable pagination (Relay specification).
type Query {
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Apollo Server - Production-ready GraphQL server:
import { ApolloServer } from 'apollo-server-express';
import { typeDefs, resolvers } from './schema';
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
user: req.user,
db: database
})
});
Apollo Client - Comprehensive caching client:
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});
const GET_USERS = gql`
query GetUsers {
users {
id
name
}
}
`;
client.query({ query: GET_USERS }).then(result => {
console.log(result.data.users);
});
GraphQL has a structured way to handle errors:
{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "USER_NOT_FOUND",
"userId": "123"
}
}
]
}
Solve the N+1 query problem:
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds) => {
const users = await User.findByIds(userIds);
return userIds.map(id => users.find(user => user.id === id));
});
const resolvers = {
Post: {
author: (post) => userLoader.load(post.authorId)
}
};
Good fit:
Consider alternatives:
This guide provides a comprehensive overview of GraphQL. For the most up-to-date information and specific implementation details, always refer to the official documentation of your chosen GraphQL implementation.