What is GraphQL?
GraphQL is a query language that defines how a client can request information via an API. Developers use the GraphQL syntax to request specific data and return it from several sources. Once a client describes the required data structure, the server returns the exact structure.
GraphQL provides open source reading, monitoring, and mutating capabilities that you can use to make real-time updates to data. GraphQL servers are compatible with various coding languages, including Python, JavaScript, Ruby, Go, PHP, and C#.
GraphQL offers comprehensive visibility into data stored in an API that enables developers to get only relevant data. It also provides an architecture that makes it easier to scale and adapt APIs over time.
What are GraphQL types?
In GraphQL, a type is a way of defining the structure of the data that an API can return. For example, if we have an API that returns information about books, we might define a type called “Book” that describes the fields that every book object will have, such as the title and the author. In a GraphQL schema, a type is defined using the “type” keyword, followed by the name of the type and a set of fields enclosed in braces. Here’s an example of a “Book” type:
type Book { title: String author: String }
This defines a “Book” type that has two fields: “title” and “author”. Both fields are of type “String”, which means they will hold string values.
To perform a query that requests data from this type, we can use the “query” keyword followed by the name of the type and the fields we want to request. For example, to request the “title” and “author” fields for a book, we could use the following query:
query { book { title author } }
This query will request the “title” and “author” fields for a “book” object, and the server will return the data for those fields in the response. The response might look something like this:
{ "data": { "book": { "title": "The Great Gatsby", "author": "F. Scott Fitzgerald" } } }
In this example, the server has returned the requested data for the “book” object, with the “title” and “author” fields set to the corresponding values.
GraphQL basic types
In GraphQL, there are several basic types that can be used to define the fields in a type. These types include:
- String: a string of text
- Int: a signed 32-bit integer
- Float: a double-precision floating-point number
- Boolean: a true/false value
- ID: a unique identifier
These basic types can be used to define the fields in a GraphQL type. For example, if we want to define a type called “Book” that has fields for the title, author, and number of pages, we could use the following code:
type Book { title: String author: String pages: Int }
In this example, the “Book” type has three fields: “title”, “author”, and “pages”. The “title” and “author” fields are of type “String”, which means they will hold string values. The “pages” field is of type “Int”, which means it will hold an integer value.
We can also use basic types to define the input and output types for a GraphQL query or mutation. For example, if we want to define a mutation that updates the number of pages in a book, we could use the following code:
type Mutation { updateBookPages( bookId: ID! newPageCount: Int! ): Book }
In this example, the “updateBookPages” mutation has two input fields: “bookId” and “newPageCount”. The “bookId” field is of type “ID” and is required (indicated by the “!” after the type), which means that it must be provided in the mutation request. The “newPageCount” field is of type “Int” and is also required. The mutation’s output type is “Book”, which means that it will return a “Book” object with the updated page count.
GraphQL object types
An object type represents a complex data structure that can contain multiple fields, each with their own type. Object types can represent things like users, posts, or comments in a social media application, or products, orders, and customers in an e-commerce application.
An object type can contain fields of both static and object types, allowing you to build up complex data structures that reflect the relationships between different pieces of data in your application.
For example, you might define a “User” object type with fields for the user’s name (a string), age (an integer), and email address (a string), as well as a “Posts” field that references a list of “Post” object types.
Here is a simple example of how to create a GraphQL object type with two integer fields, and implement it with a root-level resolver:
const typeDefs = ` type Rectangle { width: Int! height: Int! } type Query { rectangle: Rectangle! } ` const resolvers = { Query: { rectangle: () => ({ width: 10, height: 20, }), }, } const server = new GraphQLServer({ typeDefs, resolvers })
This code defines a rectangle object type with two integer fields, width and height, and a rectangle query that returns an instance of the rectangle type. The rectangle resolver function returns a hard-coded rectangle object with a width of 10 and a height of 20.
Alternatively, you can implement the rectangle object type as a class with instance methods:
class Rectangle { constructor(width, height) { this.width = width this.height = height } area() { return this.width * this.height } } const typeDefs = ` type Rectangle { width: Int! height: Int! area: Int! } type Query { rectangle: Rectangle! } ` const resolvers = { Query: { rectangle: () => new Rectangle(10, 20), }, Rectangle: { area: (rectangle) => rectangle.area(), }, } const server = new GraphQLServer({ typeDefs, resolvers })
In this example, the rectangle class has a constructor that sets the width and height fields, and an area method that calculates the area of the rectangle. The rectangle resolver function returns an instance of the rectangle class, and the area resolver function is used to calculate the area of the rectangle when it is queried.
GraphQL mutations and input types
Basic mutations
In GraphQL, a mutation is a way of modifying data on the server. Mutations are used to create, update, or delete data, and they are defined in the GraphQL schema using the “mutation” keyword.
To define a mutation, we use the “mutation” keyword followed by the name of the mutation and a set of input fields enclosed in parentheses. The input fields define the data that will be provided in the mutation request, and they can have different types, such as strings, integers, or other custom types.
For example, to define a mutation that updates the number of pages in a book, we could use the following code:
type Mutation { updateBookPages( bookId: ID! newPageCount: Int! ): Book }
In this example, the “updateBookPages” mutation has two input fields: “bookId” and “newPageCount”. The “bookId” field is of type “ID” and is required (indicated by the “!” after the type), which means that it must be provided in the mutation request. The “newPageCount” field is of type “Int” and is also required. The mutation’s output type is “Book”, which means that it will return a “Book” object with the updated page count.
When defining the input fields for a mutation, we can use input types to specify the structure of the input data. An input type is similar to an object type, but it is used specifically for input data. To define an input type, we use the “input” keyword followed by the name of the input type and a set of fields enclosed in braces.
For example, to define an input type called “BookInput” that has fields for the title, author, and number of pages, we could use the following code:
input BookInput { title: String author: String pages: Int }
In this example, the “BookInput” input type has three fields: “title”, “author”, and “pages”. These fields have the same types as the corresponding fields in the “Book” object type.
Once an input type is defined, we can use it as the type for the input fields of a mutation. For example, we could update the “updateBookPages” mutation from earlier to use the “BookInput” input type like this:
type Mutation { updateBookPages( book: BookInput! ): Book }
In this example, the “updateBookPages” mutation now has a single input field called “book” that is of type “BookInput” and is required. This means that the input data for the mutation must be an object with the same fields and field types as the “BookInput” input type. The output type of the mutation is still “Book”, so the mutation will return a “Book” object with the updated page count.
Mutations with root resolvers
A root resolver is a function that resolves a field on the root query or mutation type. Root resolvers are responsible for handling the logic for querying or mutating data on the server, and they are defined in the GraphQL schema using the “resolve” keyword.
To use root resolvers with mutations, we first need to define a mutation in the schema using the “mutation” keyword. Then, we can define a root resolver for the mutation by using the “resolve” keyword followed by the name of the mutation and a function that handles the mutation logic.
For example, to define a mutation called “updateBookPages” that updates the number of pages in a book, we could use the following code:
type Mutation { updateBookPages(bookId: ID!, newPageCount: Int!): Book @resolve(fn: "updateBookPagesResolver") }
In this example, the “updateBookPages” mutation has two input fields: “bookId” and “newPageCount”. The mutation’s output type is “Book”, which means that it will return a “Book” object with the updated page count. The “updateBookPages” mutation also has a root resolver called “updateBookPagesResolver” that is defined using the “@resolve” directive.
The “updateBookPagesResolver” root resolver function will be called when a client makes a request to the “updateBookPages” mutation. The function will receive the input data for the mutation as arguments, and it should return the updated “Book” object. Here’s an example of what the “updateBookPagesResolver” function might look like:
function updateBookPagesResolver(bookId, newPageCount) { // Look up the book in the database using the provided book ID const book = findBookById(bookId); // Update the book's page count book.pages = newPageCount; // Save the updated book to the database saveBook(book); // Return the updated book return book; }
In this example, the “updateBookPagesResolver” function receives the “bookId” and “newPageCount” input data as arguments. It uses the provided book ID to look up the book in the database, updates the book’s page count, saves the updated book to the database, and finally returns the updated book. This returned “Book” object will be included in the response to the mutation request.
Once a mutation and its root resolver are defined in the GraphQL schema, a client can make a request to the mutation and provide the required input data. The server will then execute the root resolver function to handle the mutation logic and return the updated data in the response. For example, to make a request to the “updateBookPages” mutation, a client might use the following code:
const query = ` mutation { updateBookPages(bookId: 1234, newPageCount: 500) { title, author, pages } } `; let response = await fetch("/graphql", { method: "POST", body: JSON.stringify({ query }), }); const result = await response.json(); console.log(result);
Enabling GraphQL with Solo Gloo GraphQL
The Gloo GraphQL module embeds a GraphQL server natively into Gloo Mesh and Gloo Gateway enabling federated GraphQL queries of your APIs using your service mesh and API gateways. No additional GraphQL servers required!
Combine the developer-friendly GraphQL query language for APIs with Istio’s ability to secure, manage, and observe application traffic across microservices. This eliminates the need for a distinct system of servers and schema libraries of parsers and resolvers for GraphQL; all of your policies and requests can be managed in Envoy Proxy filters. Leveraging the existing capabilities Solo.io provides via Envoy, you can scale from single, monolithic GraphQL APIs to a federated graph spanning multiple clusters.