DEV Community

Cover image for GraphQL Full Guide
Harsh Mishra
Harsh Mishra

Posted on

GraphQL Full Guide

Introduction to GraphQL

GraphQL is a powerful query language for APIs and a runtime for executing those queries by using a type system you define for your data. Developed by Facebook in 2012 and open-sourced in 2015, GraphQL has transformed how APIs are designed, making them more efficient and developer-friendly.


What is GraphQL?

GraphQL is both a query language and a runtime that enables clients to request precisely the data they need from an API. Unlike traditional REST APIs, which expose multiple endpoints for different resources, GraphQL provides a single endpoint and allows clients to query multiple resources in a single request.

Definition and Overview
  • Query Language: GraphQL allows developers to describe the shape of the data they need using a syntax that is simple, yet expressive.
  • Runtime: It executes these queries against a type system (schema) defined by the server.
  • Type System: GraphQL enforces strict typing, enabling clients and servers to understand the structure of the data before any request is made.

Example Query:

{
  user(id: "1") {
    name
    email
    friends {
      name
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Example Response:

{
  "data": {
    "user": {
      "name": "John Doe",
      "email": "john.doe@example.com",
      "friends": [
        { "name": "Jane Smith" },
        { "name": "Bob Brown" }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Comparison with REST: Key Differences

Feature REST GraphQL
Endpoints Multiple endpoints for resources (e.g., /users, /posts) Single endpoint for all queries (e.g., /graphql)
Data Retrieval Fixed data structure returned by server Client specifies the exact data it needs
Over-fetching/Under-fetching Common due to rigid endpoints Eliminated; queries return precisely what is requested
Batching Requests Often requires multiple network calls Can retrieve multiple resources in a single request
Schema Evolution Breaking changes when APIs change Backward compatibility with field deprecation
Real-time Data Requires custom implementation Built-in subscriptions for real-time updates

Example Scenario: A mobile app needs a user’s name, email, and friends’ names.

  • REST: Requires three separate requests: one for the user, one for their friends, and another for the friends' details.
  • GraphQL: Retrieves all this data in a single query.

Why GraphQL?

GraphQL offers several advantages over traditional API architectures, making it the preferred choice for many developers and organizations.

Benefits
  1. Efficiency
    • Eliminates over-fetching and under-fetching of data.
    • Reduces the number of network requests.
  2. Flexibility
    • Allows clients to specify exactly what data they need.
    • Adaptable to diverse client needs without server-side changes.
  3. Simplified API Design
    • A single endpoint simplifies API management.
    • Consistent and predictable query structure.
  4. Real-time Capabilities
    • Supports subscriptions for real-time updates.
  5. Improved Developer Experience
    • Built-in introspection and robust developer tools.
    • Clear and self-documenting schemas.
Use Cases
  • E-commerce: Fetching product details, reviews, and recommendations in a single request.
  • Social Media: Combining posts, comments, and user details in one query.
  • SaaS Applications: Powering dynamic dashboards where users can query for metrics, logs, and events.
  • Mobile and IoT: Optimizing performance by limiting data sent over the network.
Industry Adoption

Organizations like Facebook, GitHub, Shopify, Twitter, and Netflix use GraphQL to enhance their API experiences. It has become a critical tool for building modern, high-performance applications across industries.


GraphQL’s unique approach to data querying and its growing ecosystem make it an indispensable tool for building efficient and flexible APIs.

Core Concepts of GraphQL

Schema

A schema in GraphQL is the backbone of any GraphQL API. It defines the structure, types, and capabilities of the API. Think of it as a contract between the client and the server, describing the data that clients can request and the operations they can perform.

What is a Schema in GraphQL?

The schema is a declarative definition of the API’s data model. It specifies:

  • The types of data available (e.g., User, Post, Product).
  • The operations clients can perform (queries and mutations).
  • Relationships between different types.

The schema is written in the GraphQL Schema Definition Language (SDL), which is a human-readable syntax for defining types and operations.

Example Schema:

type User {
  id: ID!
  name: String!
  email: String!
  friends: [User!]!
}

type Query {
  user(id: ID!): User
  allUsers: [User!]!
}

type Mutation {
  createUser(name: String!, email: String!): User
}

schema {
  query: Query
  mutation: Mutation
}
Enter fullscreen mode Exit fullscreen mode

Types, Queries, and Mutations

1. Types

Types define the structure of data in your GraphQL API. They can represent objects, lists, or scalars. The most common scalar types are:

  • Int: Integer values.
  • Float: Decimal values.
  • String: Text values.
  • Boolean: True/false values.
  • ID: Unique identifiers.

You can also define custom object types, enums, and input types.

Example:

type Product {
  id: ID!
  name: String!
  price: Float!
  inStock: Boolean!
}
Enter fullscreen mode Exit fullscreen mode
2. Queries

Queries are used to fetch data from the server. They define what data the client wants and how it should be shaped.

Example Query:

{
  product(id: "123") {
    name
    price
    inStock
  }
}
Enter fullscreen mode Exit fullscreen mode

Example Response:

{
  "data": {
    "product": {
      "name": "Wireless Headphones",
      "price": 99.99,
      "inStock": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
3. Mutations

Mutations are used to modify data on the server. They allow for creating, updating, or deleting resources.

Example Mutation:

mutation {
  createUser(name: "Jane Doe", email: "jane.doe@example.com") {
    id
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Example Response:

{
  "data": {
    "createUser": {
      "id": "1",
      "name": "Jane Doe",
      "email": "jane.doe@example.com"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The schema and its associated operations form the foundation of GraphQL, enabling clients and servers to interact in a clear, efficient, and flexible manner.

Types in GraphQL

Types in GraphQL define the structure of the data you can query or manipulate. They are the building blocks of a GraphQL schema and are critical for ensuring the clarity and predictability of an API. Let's dive into the main categories of types in GraphQL.


1. Scalar Types

Scalar types represent the primitive data types in GraphQL. They serve as the foundation for defining fields in your schema. GraphQL includes the following built-in scalar types:

  • Int: Represents a signed 32-bit integer.
  • Float: Represents a signed double-precision floating-point value.
  • String: Represents a UTF-8 string.
  • Boolean: Represents a true or false value.
  • ID: Represents a unique identifier, typically used for fetching an object or entity.

Type Modifiers

Type modifiers are used to make the types more flexible and define whether a field is required or nullable. These modifiers are added to scalar types, object types, and other types to specify constraints.

  • ! (Non-nullable): Ensures that the field cannot be null. This modifier makes a field mandatory.
  • [] (List): Indicates that the field can be an array or list of items, which can be of any type.

Here's a concise breakdown of all combinations:

  • Type!: Non-nullable type. The field must always have a value and cannot be null.

    • Example: id: ID! (ID is always required)
  • [Type]: List of nullable types. The field is a list, and each item in the list can be null.

    • Example: tags: [String] (List of strings, with some items possibly null)
  • [Type]!: Non-nullable list of nullable types. The field is a list, but the list itself cannot be null; items inside it can be null.

    • Example: tags: [String]! (List of strings, list is required, but items can be null)
  • [Type!]: List of non-nullable types. The field is a list, and neither the list nor its items can be null.

    • Example: tags: [String!] (List of strings, list and items are both required)
  • [Type!]!: Non-nullable list of non-nullable types. The field is a list, and neither the list nor its items can be null.

    • Example: tags: [String!]! (List of strings, list and items are both required)

These combinations give you fine control over how fields are represented and handled in your GraphQL schema.

Example: Using Scalar Types

type Product {
  id: ID!
  name: String!
  price: Float!
  inStock: Boolean!
  tags: [String]!
  ratings: [Float]
  categories: [String!]!
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • id is an ID!, non-nullable, unique to each product.
  • name is a String!, non-nullable.
  • price is a Float!, non-nullable, representing the product's price.
  • inStock is a Boolean!, non-nullable, indicating availability.
  • tags is a [String]!, non-nullable list, where the list must be present, but items can be null.
  • ratings is a [Float], a nullable list, where the list and items can be null.
  • categories is a [String!]!, non-nullable list of non-nullable strings, where the list and its items must both be present.

2. Custom Types

Custom types define the structure of your data model. These are object types that consist of fields, where each field has a type. They enable you to model real-world entities and their relationships.

Example: Custom Types

type User {
  id: ID!
  name: String!
  email: String!
  friends: [User!]!
}

type Product {
  id: ID!
  name: String!
  price: Float!
  owner: User
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • User is a custom type with fields such as id, name, email, and a list of friends.
  • Product is another custom type that has a relationship with User through the owner field.

Custom types can include other custom types, creating relationships between entities.


3. Enums

Enums (short for enumerations) represent a fixed set of values. They are useful for fields that can take one value out of a predefined list, ensuring that the values remain consistent.

Example: Using Enums

enum OrderStatus {
  PENDING
  COMPLETED
  CANCELLED
}

type Order {
  id: ID!
  status: OrderStatus!
  total: Float!
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • OrderStatus is an enum type with three possible values: PENDING, COMPLETED, and CANCELLED.
  • The status field in the Order type uses this enum, ensuring that its value must be one of the predefined options.

4. Input Types

Input types are used for defining the structure of the data passed as arguments to queries or mutations. They allow you to bundle multiple fields into a single argument.

Example: Using Input Types

input CreateUserInput {
  name: String!
  email: String!
}

type Mutation {
  createUser(input: CreateUserInput!): User
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • CreateUserInput is an input type that encapsulates the name and email fields.
  • The createUser mutation accepts an argument input of type CreateUserInput, simplifying the data structure for creating a new user.

Example Query Using Input Types

mutation {
  createUser(input: { name: "Alice", email: "alice@example.com" }) {
    id
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Example Response

{
  "data": {
    "createUser": {
      "id": "1",
      "name": "Alice",
      "email": "alice@example.com"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

These types—scalars, custom types, enums, and input types—provide the flexibility and precision needed to model any API's data requirements. They form the backbone of a GraphQL schema and ensure that both clients and servers can work together effectively.

Resolvers in GraphQL

Resolvers are the functions responsible for fetching the data that corresponds to a specific field in a GraphQL query or mutation. They act as the "glue" between the schema and the data, enabling dynamic and customizable responses to client requests.


The Role of Resolvers in GraphQL

When a client makes a GraphQL query, the server refers to the schema to understand the structure of the query and then calls the appropriate resolver functions to fetch the requested data. Resolvers are essential because they define how and from where the data is retrieved.

Key Responsibilities of Resolvers:

  1. Map schema fields to actual data sources (e.g., databases, APIs, or other services).
  2. Process any arguments passed in queries or mutations.
  3. Handle relationships between fields (e.g., fetching related data like a user's posts).
  4. Ensure data transformations or business logic is applied before sending a response to the client.

Resolver Function Signature:

A resolver function typically follows this signature:

(field, args, context, info) => { /* Returns the data */ }
Enter fullscreen mode Exit fullscreen mode
  • field: The parent object that contains this field (useful in nested queries).
  • args: The arguments provided by the client for the field.
  • context: Shared data across resolvers, like authentication tokens or database connections.
  • info: Metadata about the query, like execution state or schema details.

Types of Resolvers

There are three primary types of resolvers in GraphQL: Query Resolvers, Mutation Resolvers, and (optionally) Subscription Resolvers.


1. Query Resolvers

Query resolvers fetch and return data for fields in a Query. These are the most commonly used resolvers and typically handle read operations.

Example Schema:

type Query {
  getUser(id: ID!): User
  getAllProducts: [Product!]
}

type User {
  id: ID!
  name: String!
  email: String!
}

type Product {
  id: ID!
  name: String!
  price: Float!
}
Enter fullscreen mode Exit fullscreen mode

Example Query Resolvers:

const resolvers = {
  Query: {
    getUser: (parent, args, context) => {
      const { id } = args;
      return context.dataSources.userAPI.getUserById(id);
    },
    getAllProducts: (parent, args, context) => {
      return context.dataSources.productAPI.getAllProducts();
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

In this example:

  • getUser takes an id argument and fetches the corresponding user.
  • getAllProducts fetches a list of all available products.

2. Mutation Resolvers

Mutation resolvers handle create, update, or delete operations. They typically modify data on the server and return the result of the operation.

Example Schema:

type Mutation {
  createProduct(name: String!, price: Float!): Product
  updateUser(id: ID!, name: String, email: String): User
}
Enter fullscreen mode Exit fullscreen mode

Example Mutation Resolvers:

const resolvers = {
  Mutation: {
    createProduct: (parent, args, context) => {
      const { name, price } = args;
      return context.dataSources.productAPI.createProduct({ name, price });
    },
    updateUser: (parent, args, context) => {
      const { id, name, email } = args;
      return context.dataSources.userAPI.updateUser({ id, name, email });
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

In this example:

  • createProduct adds a new product and returns the newly created product object.
  • updateUser updates an existing user's details and returns the updated user.

3. Nested Resolvers

Nested resolvers are used to resolve fields that depend on parent fields. For example, when fetching a user, you might also need to fetch their related posts.

Example Schema:

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

type Post {
  id: ID!
  title: String!
  content: String!
}
Enter fullscreen mode Exit fullscreen mode

Example Nested Resolver:

const resolvers = {
  User: {
    posts: (parent, args, context) => {
      const { id } = parent; // The user ID is available in the parent object
      return context.dataSources.postAPI.getPostsByUserId(id);
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

In this example:

  • When querying for a user's posts, the posts resolver fetches all posts associated with the user's ID.

Example Query:

{
  getUser(id: "1") {
    name
    email
    posts {
      title
      content
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Example Response:

{
  "data": {
    "getUser": {
      "name": "John Doe",
      "email": "john.doe@example.com",
      "posts": [
        {
          "title": "GraphQL Basics",
          "content": "Understanding the core concepts of GraphQL."
        },
        {
          "title": "Advanced Resolvers",
          "content": "Implementing nested and complex resolvers."
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Resolvers are at the heart of GraphQL’s flexibility, allowing APIs to fetch data from diverse sources and shape responses to fit client needs. By structuring resolvers effectively, developers can ensure robust, scalable, and maintainable APIs.

Here’s a comprehensive example that combines all the concepts discussed in the article—schema definition, queries, mutations, types, and resolvers. This "all-in-one" example models a simple API for managing users and their associated posts.


Complete GraphQL Example: Users and Posts API

Schema Definition

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

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

type Query {
  getUser(id: ID!): User
  getAllUsers: [User!]!
  getPost(id: ID!): Post
  getAllPosts: [Post!]!
}

type Mutation {
  createUser(name: String!, email: String!): User
  createPost(title: String!, content: String!, authorId: ID!): Post
}
Enter fullscreen mode Exit fullscreen mode

This schema defines:

  • Two main object types: User and Post, with a relationship where a user can have multiple posts.
  • Queries for fetching users and posts, individually or as a list.
  • Mutations for creating new users and posts.

Resolvers

const resolvers = {
  Query: {
    getUser: (parent, args, context) => {
      const { id } = args;
      return context.dataSources.userAPI.getUserById(id);
    },
    getAllUsers: (parent, args, context) => {
      return context.dataSources.userAPI.getAllUsers();
    },
    getPost: (parent, args, context) => {
      const { id } = args;
      return context.dataSources.postAPI.getPostById(id);
    },
    getAllPosts: (parent, args, context) => {
      return context.dataSources.postAPI.getAllPosts();
    },
  },
  Mutation: {
    createUser: (parent, args, context) => {
      const { name, email } = args;
      return context.dataSources.userAPI.createUser({ name, email });
    },
    createPost: (parent, args, context) => {
      const { title, content, authorId } = args;
      return context.dataSources.postAPI.createPost({ title, content, authorId });
    },
  },
  User: {
    posts: (parent, args, context) => {
      const { id } = parent; // The parent is the user object
      return context.dataSources.postAPI.getPostsByUserId(id);
    },
  },
  Post: {
    author: (parent, args, context) => {
      const { authorId } = parent; // The parent is the post object
      return context.dataSources.userAPI.getUserById(authorId);
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

This resolver implementation:

  • Handles queries to fetch individual and multiple users/posts.
  • Handles mutations to create new users and posts.
  • Resolves nested fields (posts for users and author for posts).

Sample Queries and Mutations

Query: Fetch a User with Their Posts

{
  getUser(id: "1") {
    name
    email
    posts {
      title
      content
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "data": {
    "getUser": {
      "name": "Alice",
      "email": "alice@example.com",
      "posts": [
        {
          "title": "GraphQL Introduction",
          "content": "Learn the basics of GraphQL."
        },
        {
          "title": "Advanced Resolvers",
          "content": "Dive deep into resolver patterns."
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Mutation: Create a New User

mutation {
  createUser(name: "Bob", email: "bob@example.com") {
    id
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "data": {
    "createUser": {
      "id": "2",
      "name": "Bob",
      "email": "bob@example.com"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Mutation: Create a Post for a User

mutation {
  createPost(title: "GraphQL for Beginners", content: "A beginner's guide to GraphQL.", authorId: "1") {
    id
    title
    content
    author {
      name
      email
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "data": {
    "createPost": {
      "id": "101",
      "title": "GraphQL for Beginners",
      "content": "A beginner's guide to GraphQL.",
      "author": {
        "name": "Alice",
        "email": "alice@example.com"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Data Sources Implementation (Example)

For simplicity, imagine the data is stored in-memory or retrieved from a database. Here's an example of how userAPI and postAPI might look:

const users = [
  { id: "1", name: "Alice", email: "alice@example.com" },
];

const posts = [
  { id: "201", title: "GraphQL Basics", content: "Introduction to GraphQL.", authorId: "1" },
];

const userAPI = {
  getUserById: (id) => users.find((user) => user.id === id),
  getAllUsers: () => users,
  createUser: ({ name, email }) => {
    const newUser = { id: String(users.length + 1), name, email };
    users.push(newUser);
    return newUser;
  },
};

const postAPI = {
  getPostById: (id) => posts.find((post) => post.id === id),
  getAllPosts: () => posts,
  getPostsByUserId: (userId) => posts.filter((post) => post.authorId === userId),
  createPost: ({ title, content, authorId }) => {
    const newPost = { id: String(posts.length + 1), title, content, authorId };
    posts.push(newPost);
    return newPost;
  },
};

module.exports = { userAPI, postAPI };
Enter fullscreen mode Exit fullscreen mode

This example integrates schema, resolvers, and queries/mutations to create a fully functional GraphQL API. It showcases how to fetch nested relationships, handle arguments, and implement core concepts like queries, mutations, and custom types.

Queries and Mutations in GraphQL

GraphQL revolves around two primary operations: queries and mutations. Queries are used to fetch data from the server, while mutations allow clients to modify or create data. Understanding how to write and use these operations is fundamental to working with GraphQL.


Queries: Fetching Data

A query is a read-only operation that retrieves data based on the structure defined in the schema. Clients can request exactly the data they need, reducing the amount of unnecessary information sent over the network.

Writing a Basic Query

Example Scenario: Fetching a user and their associated posts.

Schema:

type Query {
  getUser(id: ID!): User
}

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

type Post {
  id: ID!
  title: String!
  content: String!
}
Enter fullscreen mode Exit fullscreen mode

Query:

{
  getUser(id: "1") {
    name
    email
    posts {
      title
      content
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The getUser query requires an id as an argument, specified using id: "1".
  • The client requests the name and email fields of the user and details about their posts (i.e., title and content).

Response:

{
  "data": {
    "getUser": {
      "name": "Alice",
      "email": "alice@example.com",
      "posts": [
        {
          "title": "GraphQL Basics",
          "content": "An introduction to GraphQL."
        },
        {
          "title": "Advanced Queries",
          "content": "Exploring advanced querying techniques."
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Arguments in Queries

GraphQL allows clients to pass arguments to queries to filter or customize the data they fetch.

Schema:

type Query {
  getPostsByAuthor(authorId: ID!): [Post!]
}
Enter fullscreen mode Exit fullscreen mode

Query:

{
  getPostsByAuthor(authorId: "1") {
    title
    content
  }
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "data": {
    "getPostsByAuthor": [
      {
        "title": "GraphQL Basics",
        "content": "An introduction to GraphQL."
      },
      {
        "title": "Advanced Queries",
        "content": "Exploring advanced querying techniques."
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Mutations: Modifying Data

Mutations are write operations that allow clients to create, update, or delete data on the server. They often require input arguments to define the changes or new data to be added.

Writing a Basic Mutation

Example Scenario: Creating a new user.

Schema:

type Mutation {
  createUser(name: String!, email: String!): User
}

type User {
  id: ID!
  name: String!
  email: String!
}
Enter fullscreen mode Exit fullscreen mode

Mutation:

mutation {
  createUser(name: "Bob", email: "bob@example.com") {
    id
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The createUser mutation takes name and email as arguments to create a new user.
  • The client specifies the fields they want in the response (e.g., id, name, email).

Response:

{
  "data": {
    "createUser": {
      "id": "2",
      "name": "Bob",
      "email": "bob@example.com"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Mutations with Input Types

For complex mutations, GraphQL allows using input types to bundle arguments into a single object.

Schema with Input Type:

input UserInput {
  name: String!
  email: String!
}

type Mutation {
  createUser(input: UserInput!): User
}
Enter fullscreen mode Exit fullscreen mode

Mutation:

mutation {
  createUser(input: { name: "Charlie", email: "charlie@example.com" }) {
    id
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "data": {
    "createUser": {
      "id": "3",
      "name": "Charlie",
      "email": "charlie@example.com"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Differences Between Queries and Mutations

Feature Query Mutation
Purpose Retrieve data (read-only) Modify or create data
Syntax Begins with { Begins with mutation {
Side Effects None Can have side effects (e.g., database changes)
Response Current state of the data Reflects the result of the operation

Queries and mutations are the core operations in GraphQL, enabling clients to interact with APIs efficiently. By understanding how to structure queries and use mutations effectively, developers can build dynamic, responsive applications with minimal overhead.

GraphQL Specification: A Comprehensive Guide

This section provides an in-depth explanation of the GraphQL specification with detailed examples for every concept. By the end, you'll understand how to structure and optimize GraphQL queries and mutations for various scenarios.


Structure of a GraphQL Operation

A GraphQL operation is composed of three main components:

  1. Operation Type: Indicates the operation's intent—either query or mutation.
  2. Operation Name (Optional): A unique name to identify the operation.
  3. Selection Set: Specifies the requested data, including fields and nested fields.

Operation Name

While not mandatory, operation names are highly recommended for debugging, identifying, and organizing GraphQL requests. They are especially useful when working with multiple operations in a single request.

Example: Basic Query With an Operation Name

query GetUserDetails {
  getUser(id: "1") {
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Sending Multiple Operations in a Single Request

GraphQL supports sending multiple operations in one request, but only one operation can be executed at a time. The executed operation is specified using the operationName field in the request body.

Example: Multiple Operations

query GetPrimaryUser {
  getUser(id: "1") {
    name
    email
  }
}

query GetSecondaryUser {
  getUser(id: "2") {
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Request Body:

{
  "query": "...", // Full GraphQL query
  "operationName": "GetPrimaryUser"
}
Enter fullscreen mode Exit fullscreen mode

If the operationName field is not provided in such cases, GraphQL may return an error because it doesn't support executing multiple queries simultaneously.


Variables in GraphQL

Variables enable dynamic and reusable queries or mutations by separating query structure from input values.

Example: Passing Variables

Schema:

type Query {
  getUser(id: ID!): User
}

type User {
  id: ID!
  name: String!
  email: String!
}
Enter fullscreen mode Exit fullscreen mode

Query Without Variables:

query {
  getUser(id: "1") {
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Query With Variables:

query GetUser($userId: ID!) {
  getUser(id: $userId) {
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Variables:

{
  "userId": "1"
}
Enter fullscreen mode Exit fullscreen mode

Equivalent JavaScript Object:

const variables = {
  userId: "1",
};
Enter fullscreen mode Exit fullscreen mode

Examples of Variable Data Types

GraphQL variables support a variety of types, including scalars, enums, and input types.

1. Scalar Values

Query:

query GetUser($userId: ID!) {
  getUser(id: $userId) {
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Variables:

{
  "userId": "1"
}
Enter fullscreen mode Exit fullscreen mode

Equivalent JavaScript Object:

const variables = {
  userId: "1",
};
Enter fullscreen mode Exit fullscreen mode

2. Enums

Schema:

enum Status {
  ACTIVE
  INACTIVE
  BANNED
}

type Query {
  getUsersByStatus(status: Status!): [User!]
}
Enter fullscreen mode Exit fullscreen mode

Query:

query GetUsers($status: Status!) {
  getUsersByStatus(status: $status) {
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Variables:

{
  "status": "ACTIVE"
}
Enter fullscreen mode Exit fullscreen mode

Equivalent JavaScript Object:

const variables = {
  status: "ACTIVE",
};
Enter fullscreen mode Exit fullscreen mode

3. Input Types

Schema:

input UserInput {
  name: String!
  email: String!
}

type Mutation {
  createUser(input: UserInput!): User
}
Enter fullscreen mode Exit fullscreen mode

Mutation:

mutation CreateUser($input: UserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Variables:

{
  "input": {
    "name": "Charlie",
    "email": "charlie@example.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

Equivalent JavaScript Object:

const variables = {
  input: {
    name: "Charlie",
    email: "charlie@example.com",
  },
};
Enter fullscreen mode Exit fullscreen mode

Fields and Nested Fields

Fields specify the data you want to retrieve, and nested fields allow querying related resources in a single request.

Example: Fields and Nested Fields

Schema:

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

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

type Query {
  getUser(id: ID!): User
}
Enter fullscreen mode Exit fullscreen mode

Query:

query {
  getUser(id: "1") {
    name
    posts {
      title
      content
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "data": {
    "getUser": {
      "name": "Alice",
      "posts": [
        {
          "title": "GraphQL Basics",
          "content": "An introduction to GraphQL."
        },
        {
          "title": "Advanced Queries",
          "content": "Exploring advanced querying techniques."
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Passing Variables to Nested Fields

Sometimes, nested fields also require additional arguments. Variables can be used to dynamically pass these arguments.

Schema Update:

type Post {
  id: ID!
  title: String!
  content: String!
  tags(filter: String): [String!]!
}

type Query {
  getUser(id: ID!): User
}
Enter fullscreen mode Exit fullscreen mode

Query:

query GetUserWithFilteredPosts($userId: ID!, $filter: String) {
  getUser(id: $userId) {
    name
    posts {
      title
      tags(filter: $filter)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Variables:

{
  "userId": "1",
  "filter": "featured"
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The tags field within the posts can accept the filter variable to customize the response.

Equivalent JavaScript Object:

const variables = {
  userId: "1",
  filter: "featured",
};
Enter fullscreen mode Exit fullscreen mode

Aliases

Aliases allow renaming fields in the response or querying the same field with different arguments.

Example: Using Aliases

Query:

query {
  primaryUser: getUser(id: "1") {
    name
    email
  }
  secondaryUser: getUser(id: "2") {
    name
    email
  }
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "data": {
    "primaryUser": {
      "name": "Alice",
      "email": "alice@example.com"
    },
    "secondaryUser": {
      "name": "Bob",
      "email": "bob@example.com"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This guide covers the essential aspects of GraphQL operations, including structure, variables, fields, nested fields, and aliases. By mastering these concepts, you can efficiently design dynamic and complex queries for various application needs.

Complete GraphQL Project Example

Below is a full implementation of a GraphQL project, incorporating queries, mutations, arguments, variables, custom types, enums, input types, nested resolvers, parent arguments, and more. This implementation follows the best practices and uses Apollo Server for setup and Axios for testing.


1. Schema Definition

The schema defines the structure of the API, including custom types, enums, input types, and fields with arguments.

# Enum for filtering post categories
enum Category {
  TECH
  LIFESTYLE
  EDUCATION
}

# Post type definition
type Post {
  id: ID!
  title: String!
  content: String!
  category: Category!
  tags(filter: String): [String!]!
}

# User type definition
type User {
  id: ID!
  name: String!
  email: String!
  posts(category: Category): [Post!]! # Allows filtering posts by category
}

# Input type for creating a Post
input CreatePostInput {
  title: String!
  content: String!
  category: Category!
  userId: ID!
}

# Query type definition
type Query {
  getUser(id: ID!): User
  getPost(id: ID!): Post
}

# Mutation type definition
type Mutation {
  createPost(input: CreatePostInput!): Post
}
Enter fullscreen mode Exit fullscreen mode

2. Sample Data

The following JavaScript objects simulate a database:

const users = [
  {
    id: "1",
    name: "Alice",
    email: "alice@example.com",
    posts: [
      {
        id: "1",
        title: "GraphQL Basics",
        content: "This post covers the basics of GraphQL.",
        category: "TECH",
        tags: ["graphql", "api", "javascript"],
      },
    ],
  },
  {
    id: "2",
    name: "Bob",
    email: "bob@example.com",
    posts: [],
  },
];

let posts = [
  {
    id: "1",
    title: "GraphQL Basics",
    content: "This post covers the basics of GraphQL.",
    category: "TECH",
    tags: ["graphql", "api", "javascript"],
    userId: "1",
  },
];
Enter fullscreen mode Exit fullscreen mode

3. Resolvers

The resolvers implement the logic for fetching and manipulating data. Nested resolvers handle parent objects and arguments.

const { v4: uuidv4 } = require("uuid");

const resolvers = {
  Query: {
    // Fetch a user by ID
    getUser: (parentObj, args) => {
      const { id } = args;
      return users.find(user => user.id === id);
    },
    // Fetch a post by ID
    getPost: (parentObj, args) => {
      const { id } = args;
      return posts.find(post => post.id === id);
    },
  },
  Mutation: {
    // Create a new post
    createPost: (parentObj, args) => {
      const { input } = args;
      const { title, content, category, userId } = input;

      const newPost = {
        id: uuidv4(),
        title,
        content,
        category,
        tags: [],
        userId,
      };

      posts.push(newPost);

      const user = users.find(user => user.id === userId);
      if (user) {
        user.posts.push(newPost);
      }

      return newPost;
    },
  },
  User: {
    // Fetch posts for a user, optionally filtered by category
    posts: (parentObj, args) => {
      const { category } = args;
      return posts.filter(
        post => post.userId === parentObj.id && (!category || post.category === category)
      );
    },
  },
  Post: {
    // Filter tags for a post
    tags: (parentObj, args) => {
      const { filter } = args;
      if (!filter) {
        return parentObj.tags;
      }
      return parentObj.tags.filter(tag => tag.includes(filter));
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

4. Setting Up the Server

We use Apollo Server to create and start the GraphQL server.

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

// Define the schema
const typeDefs = gql`
  enum Category {
    TECH
    LIFESTYLE
    EDUCATION
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    category: Category!
    tags(filter: String): [String!]!
  }

  type User {
    id: ID!
    name: String!
    email: String!
    posts(category: Category): [Post!]!
  }

  input CreatePostInput {
    title: String!
    content: String!
    category: Category!
    userId: ID!
  }

  type Query {
    getUser(id: ID!): User
    getPost(id: ID!): Post
  }

  type Mutation {
    createPost(input: CreatePostInput!): Post
  }
`;

// Create and start the server
const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});
Enter fullscreen mode Exit fullscreen mode

5. Example Queries and Mutations

Query to Fetch User and Filtered Posts

query GetUserWithPosts($userId: ID!, $category: Category, $filter: String) {
  getUser(id: $userId) {
    name
    email
    posts(category: $category) {
      title
      category
      tags(filter: $filter)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Variables:

{
  "userId": "1",
  "category": "TECH",
  "filter": "graphql"
}
Enter fullscreen mode Exit fullscreen mode

Mutation to Create a New Post

mutation CreateNewPost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    content
    category
  }
}
Enter fullscreen mode Exit fullscreen mode

Variables:

{
  "input": {
    "title": "Learning GraphQL",
    "content": "This post is about learning GraphQL step by step.",
    "category": "EDUCATION",
    "userId": "2"
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Axios Request Examples

Query Example

const axios = require("axios");

const query = `
  query GetUserWithPosts($userId: ID!, $category: Category, $filter: String) {
    getUser(id: $userId) {
      name
      email
      posts(category: $category) {
        title
        category
        tags(filter: $filter)
      }
    }
  }
`;

const variables = {
  userId: "1",
  category: "TECH",
  filter: "graphql",
};

axios
  .post("http://localhost:4000/", { query, variables })
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error(error);
  });
Enter fullscreen mode Exit fullscreen mode

Mutation Example

const mutation = `
  mutation CreateNewPost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
      content
      category
    }
  }
`;

const variables = {
  input: {
    title: "Learning GraphQL",
    content: "This post is about learning GraphQL step by step.",
    category: "EDUCATION",
    userId: "2",
  },
};

axios
  .post("http://localhost:4000/", { query: mutation, variables })
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error(error);
  });
Enter fullscreen mode Exit fullscreen mode

Conclusion

This comprehensive example demonstrates how to create a fully functional GraphQL API, showcasing advanced features like custom types, enums, nested resolvers, variables, and parent arguments. By leveraging Apollo Server and testing with Axios, you can implement and query a scalable GraphQL backend.

Top comments (0)