How to define GraphQL Schemas for Queries & Mutations with Resolvers

In this post, I am going to talk to you about how to define GraphQL schemas for Queries and Mutations, and then we will also learn to how implement resolvers for Query and Mutation Types. If you are new to this post, I encourage you to read this overview on GraphQL, and learn to set up the GraphQL server so that you can have a better understanding of this post.

First of all, we need to be familiar with the GraphQL Type system and the Schama definition language. So, let’s start with the GraphQL Type system.

GraphQL Type system

GraphQL has a static type system. This means it enforces type-checking during the schema definition and query validation phases. The type system in GraphQL is based on a schema that describes the available types and their relationships. It allows you to define custom object types, scalar types (such as Int, Float, String, Boolean), enumeration types, and more.

The type system enables clients to specify precisely what data they need from the server and provides strong typing guarantees, making it easier to understand and work with GraphQL APIs.

TypeDescription
ScalarPrimitive data types.
- Int: A signed 32-bit integer.
- Float: A signed double-precision floating-point value.
- String: A sequence of Unicode characters.
- Boolean: Represents true or false.
- ID: A unique identifier, often serialized as a string.
ObjectUser-defined complex types representing objects with fields and relationships
InterfaceDefines a contract for fields that an object type must include
UnionRepresents a type that can be one of several possible object types
EnumRepresents a set of discrete values
Input ObjectComplex objects used as arguments for mutations or query variables

Schema definition Language

Schemas are written in Schema Definition Language( SDL )/GraphQL schema language. It uses language-agnostic syntax to define GraphQL schemas. SDL provides a concise and human-readable way to describe the types, fields, relationships, and operations in a GraphQL API.

You can define object types, scalar types, enumeration types, interface types, union types, and input object types using SDL. You can specify the fields within each type, their types, and any arguments they accept. SDL also allows you to define the relationships between types, such as fields that reference other types.

The table below shows some of the common keywords in SDL.

KeywordDescription
typeDefines a custom object type
interfaceDefines a set of fields that an object type must implement
enumDefines a set of possible values for a field
unionDefines a type that can represent one of several object types
scalarDefines a custom scalar type (e.g. DateTime)
inputDefines an input object type
extendExtends an existing type with additional fields or interfaces
directiveDefines a custom directive

What are the building blocks of GraphQL schema?

GraphQL schema can have the following main components 

  1. Object Types: These define the type of data that is returned by the GraphQL API and the fields that are available on each type.
  2. Fields: These are the properties of an object type that can be queried by a client.
  3. Queries: These define the entry points into the GraphQL API, and specify the object types that can be queried.
  4. Mutations: These are similar to queries, but allow clients to modify or create data on the server.
  5. Subscriptions: These allow clients to subscribe to real-time updates from the server.

In this post, I focus on discussing Object Type, Fields, Queries, Mutations, and the resolvers to respond to queries and mutations.

Note:

I am also using GraphQL query language to run queries and mutations. GraphQL query language is commonly used to communicate with GraphQL servers. It provides a standardized syntax for clients to request and retrieve data from the server. We will discuss GraphQL query language in another post in detail.

It’s important to note that GraphQL query language is not the same as Schema Definition Language (SDL). SDL is used to define the schema of a GraphQL server, specifying the available types, fields, and their relationships, while the GraphQL query language is used by clients to send queries to the server, specifying the data they need and the structure of the response.

Object Types, Fields, and Arguments

Object types are one of the most essential components in a Schema. It is a representation of the object present in your API. For example, if you have an object named Author(which could be mapped to the data on authors in your data source), you can use the following object type in your schema to represent the Author object of your API.

type Author {
  id: ID!
  name: String!
  age: Int
}

Fields are used in GraphQL to describe the data and type of data that can be queried in a particular object type. In addition to the data type, fields can also be marked as nullable or non-nullable using the “!” symbol in the schema. In the example given, the “name” field in the “Author” object type is non-nullable while the “age” field is nullable.

Arguments

Arguments are another building block in Schemas, which are used to modify the behavior of field or filter results when querying data.

type Author {
  id: ID!
  name: String!
  books( limit:int = 3 ): [Book!]! # A list of books for the specified author
  age: Int
}

In the above example, the books the field takes one argument: limit. And the default value of it is 3. This field will return an array( list ) of Book objects. In SDL, it is denoted by [ ]. Note that elements and the list are non-nullable.

Assume that you want to fetch the books written by a certain author whose id is “A2”. The query would be

query Author {
  authors{
    books(limit : 2) {
      title
    }
  }
}

This query fetches a list of books written by each author. But, it will limit it to two books per author. So, even if the author has more than two books, you can see only two books for a particular author.

Queries

Queries( or Query Type ) are a critical component of a schema. By specifying the fields and their return types, query types define the entry point of every GraphQL query. So, what does this mean?

This means when a client sends a GraphQL request to a server, the request must include at least one query operation that begins with a field defined in the query type. This allows the client to fetch the desired data from the server by traversing the GraphQL type system starting from the query type. 

If that’s too confusing for you, just know that you must have at least one Query Type in your schema.

For the  Author type introduced above, you can define a Query type as below:

type Query {
  author(id: ID!): Author!
}

This defines a Query object with a single field author. The author field takes an id argument of type ID! (non-nullable ID) and returns an object of type Author. This would allow a client to query for a specific author by their ID.

The author field takes an id argument of type ID! (non-nullable ID) and returns an object of type Author. This would allow a client to query for a specific author by their ID.

query {
  author(id: "A2") {
    name
    age
  }
}

let’s assume that you want to fetch all authors, then you can modify the Query type i the Schema definition as below.

type Query {
    
   // Return all authors
   authors: () => authors,

    // Find an author by ID and return them
    author: (parent, args) => authors.find(author => author.id === args.id)
}

Mutations

Mutations are similar to the Query type. The main difference is that you use this type to edit or create new data on the server. Therefore, it is the entry point for write operations in GraphQL. So, how do you define a Mutation in the Schema?

type Mutation {
  createAuthor(name: String!, age: Int): Author!
}


Here, the mutation type has a single field named createAuthor, which is a function. This function takes two non-nullable arguments and returns the Author object.

you can also write mutations for updating and deleting as well.

type Mutation {
      createAuthor(name: String!, age: Int): Author!
      updateAuthor(id: ID!, name: String, age: Int): Author!
      deleteAuthor(id: ID!): Author!
}

So, now you wonder, how you can run a mutation in the client side with GraphQL query language. let’s see how to run the createAuthor Mutation.

mutation {
  createAuthor(name: "Stephen King", age: 75) {
    id
    name
    age
  }
}

This will create a new author with the name “Stephen King” and the age of 75, and return the id, name, and age fields of the newly created author object.

Resolvers for Queries and Mutations

So far, what we learned is how to write definitions for queries and mutations. But, definitions do not make these components work. We have not defined how these queries and mutations should respond or work. This is where the resolvers come into action.

Resolvers are functions that describe how to respond to queries and mutations defined in the schema.

How to write resolvers for queries

Resolver to get all the authors.

Take a look at the Schema below

type Author {
  id: ID!
  name: String! # returns a String and non-nullable
  age:Int
}

type Query {
  authors: [Author!]!
}

For the purpose of learning, take a look at the following dataset.

const authors = [
  { id: "1", name: "J.K. Rowling" , age:null},
  { id: "2", name: "Stephen King",age : 75 },
  { id: "3", name: "Haruki Murakami",age:74 },
];

The Query definition has one field: authors. It will return an array of Author objects.

So, what is the resolver for this?

const resolvers = {
  Query: {
        authors: () => authors
     }
}

Resolver is nothing, but a function. For example, this function returns all the authors from the dataset when you run the following query:

query Authors {
  authors {
    id
    name
    age
  }
}

It is not necessary to add all the fields to run the query. But, you should at least have one.

Resolver to get a particular author by id

Now, we will see how to write a resolver to fetch a specific author. I added a new field to the Query type to find an author by the id.

type Author {
  id: ID!
  name: String! 
  age:Int
}

type Query {
  authors: [Author!]!
  author(id: ID!): Author!
}

The author field accepts one argument and returns an Author object. Both id and return value does not contain null values. If there is any, there will be an execution error.

Assuming that we need to know about the Author with the id equal to “A2”, the query would be

query {
  author(id: “A2”) {
    name
    age
  }
}

So, what would be the resolver function that responds and populates data for this query?

author: (parent, args) => authors.find(author => author.id === args.id)

Here, we are utilizing the args argument. It is an object. This object contains all the arguments provided for a particular field. 

In our case, we have the author(id: “A2”) in the query. Therefore, the args object contains { “id”:”A2” }. So, that is why we are using “args.id” in the resolver function.

At this point, we have two resolvers

const resolvers = {
  Query: {
        // Return all authors
        authors: () => authors,
       
        // Find an author by ID and returns
        author: (parent, args) => authors.find(author => author.id === args.id)
     }
}

Resolver for books on the Author Type

The Schema I showed you above is not yet complete. The Author object has one more field. Let’s add this.

type Author {
  id: ID!
  name: String! # returns a String and non-nullable
  books(limit: Int = 3 ): [Book!]!# A list of Books
  age:Int
}

Because it has a Book Type, we need to introduce this new object type in the Schema definition.

type Book {
  id: ID!
  title: String!
  publicationDate: String
}

For the purpose of learning take a look at the following book dataset.

const books = [
  { id: "B1", title: "Harry Potter and the Philosopher's Stone", authorId: "A1" },
  { id: "B2", title: "The Shining", authorId: "A2" },
  { id: "B3", title: "1Q84", authorId: "A3" },
  { id: "B4", title: "The Stand", authorId: "A2" },
  { id: "B5", title: "Norwegian Wood", authorId: "A3" },
  { id: "B6", title: "The Green Mile", authorId: "A2" }
];

If you want to get the books written by a particular Author, you can run the following query:

query{
  author(id:"A1"){
    books{
      title
    }
  }
}

But, this will not work unless you add another resolver.  When you run the above query, you will first call the resolver we mentioned before.

author: (parent, args) => authors.find(author => author.id === args.id)

This Query resolver responds to the author(id:"A1")’ part of the query that returns data related to the id "A2“.

{ id: 'A2', name: 'Stephen King', age: 75 }

What about the  "books{ title }" part of the query?

We have not yet added a revolver to respond to this part of the Query. 

The “books” is a field of Author object type in the schema definition. Therefore, we are going to add a revolver for Author object type.

 Author: {
  // Return all books written by the author
    books: (parent , args ) => {
      console.log( parent )
      return books.filter(book => book.authorId === parent.id).slice( 0, args.limit )
    }
  }

When the server encounters the books{ title } part of the query, it knows that it needs to resolve the books field. Since “books” is a field of the Author Type in the schema, it will check the resolver for books under the Author type to resolve the field.

In the above resolver, the parent argument refers to the results of the previous resolver. Therefore, the parent argument contains { id: 'A2', name: 'Stephen King', age: 75 }.  The filter in the resolver compares the id of this with the "authorIdof the book objects and fetched the books related to “A2” author and returns it.

How to write resolvers for Mutations

We have written the schema definition for mutation and how to run a mutation on the client side. But, this does not mean your mutation is going to work. To respond to your mutation, you need a revolver. The following resolver is for the createAuthor mutation. I have included this mutation resolver with other query resolvers we discussed above.

const reolvers = {
Query:{
//resolvers for Queries
},

Mutation: {
    createAuthor: (parent, args) => {
   
      const newAuthor = {
        id: "A"+  ( authors.length + 1 ),//
        name: args.name,
        age: args.age,
        books: []
      };
      authors.push(newAuthor);
      return newAuthor;
    }
  }
}

When you run this mutation, it will add a new Author, But, remember, it will be temporary since we are not saving in permanent storage. Therefore, you can run another query to see if the new author is added.

Finally, these are all the resolvers for queries and mutations.

const resolvers = {
  // Define resolvers for queries
  Query: {
    
    authors: () => authors,
    // Find an author by ID and return them
    author: (parent, args) => authors.find(author => author.id === args.id)
  },
  // Define resolvers for Author type fields
  Author: {
  // Return all books written by the author 
    books: (parent , args ) => {
      console.log( parent )
      return books.filter(book => book.authorId === parent.id).slice( 0, args.limit )
    }
  },
 
  Mutation: {
    createAuthor: (parent, args) => {   
      const newAuthor = {
        id: "A"+  ( authors.length + 1 ),
        name: args.name,
        age: args.age,
        books: []
      };
      authors.push(newAuthor);
      return newAuthor;
    }
  }
};

The following video shows you how you can run queries and mutations in the Apollo GraphQl server UI.

Wrapping up


In the post, I explained the GraphQL type system and its key components, including SDL, Object Types, Fields, arguments, Queries, and Mutations. We utilized SDL to define Query and Mutation Types in the Schema definition. Additionally, I covered the implementation of resolvers for these components, ensuring that queries and mutations generate the desired responses.

Resources

GraphQL schema and Type system

GraphQL Schema Basics

Download the code

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top