web-dev4 min read

GraphQL Tutorial: Learn API Query Language from Scratch (2026)

GraphQL Tutorial: Learn API Query Language from Scratch (2026)

Published:  |  Category: Web Dev  |  Reading time: ~15 min
GraphQL Tutorial: Learn API Query Language from Scratch (2026)

GraphQL is a query language and runtime for APIs developed by Facebook in 2012 and open-sourced in 2015. Unlike REST, where each endpoint returns a fixed data structure, GraphQL lets clients request exactly the data they need — eliminating over-fetching and under-fetching.

A GraphQL API exposes a single endpoint and responds to queries, mutations, and subscriptions. The schema, defined using the GraphQL Schema Definition Language (SDL), serves as a contract between client and server.

GraphQL Schema and Type System

The schema defines types, relationships, and entry points. Types include Query (read operations), Mutation (write operations), and Subscription (real-time events). Scalar types are String, Int, Float, Boolean, and ID.

Non-null fields are marked with ! and lists use square brackets. Input types define mutation arguments. The schema is enforced at runtime — every query is validated before execution.

type Query {
  posts(page: Int, limit: Int): [Post!]!
  post(id: ID!): Post
  users: [User!]!
}

type Mutation {
  createPost(input: PostInput!): Post!
  deletePost(id: ID!): Boolean!
}

type Post {
  id: ID!
  title: String!
  body: String!
  author: User!
  createdAt: String!
}

input PostInput {
  title: String!
  body: String!
  authorId: ID!
}

Queries and Field Selection

Queries allow clients to specify exactly which fields they need. The response mirrors the query structure. Arguments filter or paginate data at the field level. Fragments capture reusable field selections.

Directives like @include and @skip conditionally control fields. The __typename meta-field returns the object type name for client cache identification.

query GetPostsWithAuthors {
  posts(page: 1, limit: 5) {
    id
    title
    body
    author { name email }
    createdAt
  }
}

fragment PostFields on Post {
  id
  title
  body
  createdAt
}

Mutations for Data Modification

Mutations create, update, and delete data. Unlike queries (which run in parallel), mutations execute sequentially. Each mutation defines input arguments and returns the modified object for cache updates.

Apollo Client uses this pattern for optimistic updates where the UI reflects changes immediately while the server confirms.

mutation CreateNewPost {
  createPost(input: { title: "GraphQL Basics", body: "Learning GraphQL...", authorId: "1" }) {
    id
    title
    createdAt
  }
}

mutation DeleteOldPost($postId: ID!) {
  deletePost(id: $postId)
}

Resolvers: Connecting Schema to Data

Resolvers are functions that return data for each field in the schema. Every field has a resolver; the default simply returns the value from the parent object. Custom resolvers fetch from databases, REST APIs, or other services.

The context object stores authenticated user data, database connections, and DataLoaders. DataLoader batches and caches queries to solve the N+1 problem.

const resolvers = {
  Query: {
    posts: async (_, { page, limit }, { db }) => {
      return await db.posts.findMany({
        skip: (page - 1) * limit,
        take: limit
      });
    },
    post: async (_, { id }, { db }) => {
      return await db.posts.findUnique({ where: { id } });
    }
  },
  Post: {
    author: async (parent, _, { db }) => {
      return await db.users.findUnique({ where: { id: parent.authorId } });
    }
  }
};

Subscriptions for Real-Time Data

Subscriptions enable real-time communication via WebSocket connections. When data changes, the server pushes updates to all subscribed clients. Subscriptions use the same schema type system as queries and mutations.

Most servers support subscriptions with PubSub systems. Redis PubSub or RabbitMQ scales subscriptions across multiple server instances.

type Subscription {
  postCreated: Post!
  postUpdated: Post!
}

const resolvers = {
  Subscription: {
    postCreated: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['POST_CREATED'])
    }
  },
  Mutation: {
    createPost: async (_, { input }, { db, pubsub }) => {
      const post = await db.posts.create({ data: input });
      pubsub.publish('POST_CREATED', { postCreated: post });
      return post;
    }
  }
};

Apollo Client Integration

Apollo Client is the most popular GraphQL client. It manages a normalized cache that automatically updates when mutations return modified objects. Queries use the gql template literal and the useQuery hook.

InMemoryCache normalizes data by type and ID, merging results from different queries. Apollo supports optimistic updates, polling, and pagination.

import { ApolloClient, InMemoryCache, gql, useQuery } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache()
});

const GET_POSTS = gql`
  query GetPosts { posts { id title author { name } } }
`;

function PostList() {
  const { loading, error, data } = useQuery(GET_POSTS);
  if (loading) return 

Loading...

; if (error) return

Error: {error.message}

; return data.posts.map(post =>
{post.title}
); }

Frequently Asked Questions

Is GraphQL a replacement for REST?

Not exactly. GraphQL complements REST. REST excels with simple resource-based APIs and HTTP caching. GraphQL shines in complex UIs requiring data from multiple sources. Many teams run both.

How does GraphQL handle file uploads?

GraphQL does not natively support file uploads, but the multipart request specification enables it via the Upload scalar type.

What is the N+1 problem in GraphQL?

The N+1 problem occurs when resolving a list triggers additional queries for nested data. For example, fetching 10 posts and their authors causes 11 queries. DataLoader solves this by batching individual keys and caching per request.

Can I secure a GraphQL endpoint?

Yes. Authentication works identically to REST — verify tokens in request headers. Authorization is implemented at the resolver level. Rate limiting should consider query complexity rather than raw request count.

Originally published on Ayodhyyya. Last updated June 1, 2026.