Skip to content
Home
GraphQL Beginner's Guide: Queries, Mutations, Schemas

GraphQL Beginner's Guide: Queries, Mutations, Schemas

API Development API Development 9 min read 1851 words Intermediate ExcellentWiki Editorial Team

GraphQL is a query language for APIs developed by Facebook in 2012 and open-sourced in 2015. Unlike REST, where the server defines the response structure, GraphQL lets clients request exactly the data they need — nothing more, nothing less. This shift from server-driven to client-driven data fetching addresses fundamental problems that emerge at scale: over-fetching (receiving more data than needed), under-fetching (needing multiple round trips to gather all required data), and tight coupling between frontend and backend development cycles.

Since its open-source release, GraphQL has been adopted by major tech companies including GitHub, Shopify, Netflix, Airbnb, and Twitter. The GraphQL Foundation, under the Linux Foundation, now stewards the specification. According to the 2024 State of GraphQL survey, 78% of developers using GraphQL report improved developer productivity, and 63% report reduced data transfer over the network.

This guide introduces GraphQL fundamentals: queries, mutations, resolvers, schema design, and when to choose GraphQL over REST.

Core Concepts

Schema

The schema is the heart of every GraphQL API. It defines the types, queries, and mutations available to clients. GraphQL uses its own type system called Schema Definition Language (SDL). Unlike REST, where endpoints implicitly define the available data shapes, GraphQL schemas provide an explicit, machine-readable contract that enables powerful tooling through introspection.

type Query {
  user(id: ID!): User
  users: [User!]!
---

type Mutation {
  createUser(input: CreateUserInput!): User!
---

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
---

type Post {
  id: ID!
  title: String!
  content: String!
---

input CreateUserInput {
  name: String!
  email: String!
  password: String!
---

The exclamation mark (!) means non-nullable. [User!]! means the list itself cannot be null and none of its elements can be null. GraphQL’s type system includes scalar types (Int, Float, String, Boolean, ID), object types, input types, enum types, union types, and interface types. Custom scalars (e.g., DateTime, URL, JSON) can be defined for domain-specific values.

Queries

Queries fetch data. The client specifies exactly which fields it needs:

query GetUser {
  user(id: "42") {
    name
    email
    posts {
      title
    }
  }
---

The response mirrors the query structure:

{
  "data": {
    "user": {
      "name": "Alice Smith",
      "email": "alice@example.com",
      "posts": [
        { "title": "GraphQL Intro" },
        { "title": "Advanced Queries" }
      ]
    }
  }
---

This eliminates over-fetching (getting too much data) and under-fetching (having to make multiple requests). One GraphQL query can replace multiple REST endpoints. The response structure always matches the query structure, making client-side data handling predictable and type-safe.

Queries can include variables for parameterization, directives for conditional inclusion (@include, @skip), aliases for renaming fields, and fragments for reusable field selections. These features, combined with the type system, give clients unprecedented control over data fetching.

Mutations

Mutations modify data. They work like queries but represent side effects:

mutation CreateUser {
  createUser(input: { name: "Bob", email: "bob@example.com" }) {
    id
    name
    email
  }
---

By convention, mutations return the modified object so the client can update its cache without an additional query. Apollo Client, the most popular GraphQL client library, automatically updates its normalized cache from mutation responses. Mutations can accept input types (distinct from regular types to enforce discipline around write operations) and can execute multiple mutations in a single request, though they run sequentially.

Subscriptions

Subscriptions provide real-time updates over WebSocket:

subscription OnUserCreated {
  userCreated {
    id
    name
  }
---

Subscriptions use the same schema and response format as queries but maintain a persistent connection. When data changes on the server, new data is pushed to all active subscription listeners. Subscriptions enable real-time features like live updates, notifications, and collaborative editing. Apollo Client supports subscriptions through WebSocket link and integrates with the normalized cache for seamless updates.

Setting Up a GraphQL Server

Using Apollo Server (JavaScript/TypeScript), the most popular GraphQL server implementation:

const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => 'Hello, world!',
  },
---;

const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
  console.log(`Server running at ${url}`);
---);

Apollo Server supports Express, Fastify, Lambda, and other platforms. For production deployments, use Apollo Server with Apollo Studio for schema management, performance tracing, and team collaboration. Alternative GraphQL servers include graphql-yoga (Prisma), graphql-ruby, graphql-java, and Strawberry (Python).

Resolvers

Resolvers are functions that return data for each field in the schema. They can fetch from databases, call other APIs, or compute values. Every field in the schema maps to a resolver (either explicit or a default resolver that reads the property from the parent object).

const resolvers = {
  Query: {
    user: (parent, args, context) => {
      return db.users.findById(args.id);
    },
    users: (parent, args, context) => {
      return db.users.findAll();
    },
  },
  User: {
    posts: (parent, args, context) => {
      return db.posts.findByUserId(parent.id);
    },
  },
---;

The resolver function signature is (parent, args, context, info). The parent parameter contains the result of the parent resolver (useful for resolving nested types). args contains the query arguments. context is shared across all resolvers in a request (used for authentication, data loaders, database connections). info contains query execution metadata.

The N+1 Problem

Without optimization, resolving users { posts { title } } performs one query for users and N queries for posts — this is the N+1 problem, one of the most common performance issues in GraphQL APIs. Use DataLoader to batch and cache database requests:

const DataLoader = require('dataloader');

const postLoader = new DataLoader(async (userIds) => {
  const posts = await db.posts.findByUserIds(userIds);
  return userIds.map(id => posts.filter(p => p.userId === id));
---);

DataLoader, developed by Lee Byron at Facebook, coalesces individual requests into a single batch query and caches results within a single request. It transforms N+1 queries into 2 queries, regardless of N. DataLoader also provides per-request caching, preventing duplicate fetches when the same entity is requested multiple times in a single query.

Schema Design Best Practices

Naming Conventions

Use camelCase for fields (GraphQL convention), PascalCase for types, and descriptive names that communicate business meaning. Use singular names for types (e.g., User not Users) since the type represents a single entity. Use plural names for fields that return lists (e.g., users, posts).

Use Input Types for Mutations

input CreateUserInput {
  name: String!
  email: String!
  password: String!
---

type Mutation {
  createUser(input: CreateUserInput!): User!
---

Input types are distinct from regular types because they may include fields that should never be exposed in queries (like passwords) and may exclude computed fields (like id that is generated server-side). Always use input types for mutation arguments rather than flat argument lists — this keeps mutations extensible and backward compatible.

Pagination with Connections

For list fields, use the Relay Connection specification which provides cursor-based pagination:

type Query {
  users(first: Int, after: String): UserConnection!
---

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
---

type UserEdge {
  node: User!
  cursor: String!
---

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
---

Cursor-based pagination is more reliable than offset-based pagination — it handles inserts and deletes gracefully because the cursor points to a position in the data set rather than a numerical offset. The Relay Connection spec is the industry standard for GraphQL pagination and is supported by all major GraphQL tools and clients.

Error Handling

Use a union type or interface for mutation responses to communicate both success and error states:

union CreateUserResult = User | ValidationError

type ValidationError {
  field: String!
  message: String!
---

This pattern, known as the “result type” pattern, makes error handling part of the type system rather than relying on null fields or HTTP status codes. Clients can query __typename on the result to determine whether the operation succeeded and access appropriate fields.

When to Use GraphQL vs REST

GraphQL Shines When

  • Multiple clients (mobile, web, API) need different data shapes from the same data source
  • Client requirements change frequently and you want to avoid backend changes for every new UI component
  • You need real-time updates via subscriptions with the same query syntax
  • You want strongly typed contracts between client and server that enable powerful developer tooling
  • Your application has complex data relationships that would require multiple REST round trips

REST Is Better When

  • Simple CRUD APIs with consistent data shapes and predictable access patterns
  • Heavy caching is required — HTTP caching is straightforward with REST (CDN, reverse proxy, ETag) but complex with GraphQL
  • File uploads and binary data transfer — REST’s multipart handling is mature; GraphQL requires custom solutions
  • The API will be consumed by low-capability clients or systems that cannot process GraphQL responses
  • Your team is small and needs to ship quickly — REST’s simplicity has lower initial learning investment

Conclusion

GraphQL is a powerful alternative to REST for modern API development. Its type system, client-driven queries, and real-time capabilities make it ideal for complex applications with diverse client needs. Start with Apollo Server (Node.js) or graphql-ruby (Ruby) or graphql-java (Java) — all have excellent tooling and documentation.

Frequently Asked Questions

How does GraphQL handle file uploads?

GraphQL does not have native file upload support in the specification. The most common solution is the graphql-upload package for Node.js or using multipart request spec (Apollo) where uploads are sent as multipart form data with the GraphQL operation in a separate part. Alternatively, upload files to a presigned URL (S3, GCS) and send the resulting URL as a string field in the mutation.

Is GraphQL slower than REST?

It depends on the use case. GraphQL queries can be slower than equivalent REST calls because of the overhead of parsing the query, validating it against the schema, and executing resolvers for each requested field. However, GraphQL often wins overall because it eliminates multiple round trips — one GraphQL query can replace 3-5 REST calls. The performance trade-off favors GraphQL for complex, relationship-heavy data access.

How do I secure a GraphQL API?

Implement authentication and authorization at the resolver or schema level. Use GraphQL-specific security measures: query depth limiting (prevent deeply nested queries), query cost analysis (assign costs to fields and reject expensive queries), and rate limiting based on query complexity. Apollo Studio provides built-in operation safelisting. Never expose introspection in production unless you have specific tooling needs.

What is GraphQL introspection?

Introspection is a built-in GraphQL feature that lets clients query the schema itself using __schema and __type meta-fields. Tools like GraphiQL and Apollo Studio Explorer use introspection to provide autocomplete, documentation, and schema browsing. Introspection is essential for development but should be disabled in production unless clients need it for dynamic schema discovery.

Do I need a GraphQL client library?

Not strictly, but a client library significantly improves the developer experience. Apollo Client provides normalized caching, optimistic updates, pagination helpers, and subscription support. urql is a lighter alternative with similar features. For simple use cases, raw fetch requests with proper query construction work fine. Client libraries add about 20-50 KB to your bundle, so evaluate the trade-off for your application.

For more on RESTful API design, see the REST API Design Guide. For comparing paradigms, read REST vs GraphQL Comparison. For API-first development, see API-First Development Approach.

For a comprehensive overview, read our article on Api Authentication Guide.

For a comprehensive overview, read our article on Api Caching Strategies.

Section: API Development 1851 words 9 min read Intermediate 756 articles in section Report inaccuracy Back to top