In this chapter, we'll learn about GraphQL, one of the hottest web technologies currently being used. Many large companies have adopted GraphQL for their APIs, including companies such as Facebook, Twitter, New York Times, and GitHub. We'll learn what makes GraphQL so popular, how it works internally, and how we can take advantage of its features.
In this chapter, we're going to cover the following main topics:
You should have a basic understanding of web development using Node. We will once again be using Node and Visual Studio Code.
The GitHub repository is at https://github.com/PacktPublishing/Full-Stack-React-TypeScript-and-Node. Use the code in the Chap9 folder.
To set up the Chap9 code folder, go to your HandsOnTypescript folder and create a new folder called Chap9.
In this section, we will explore what GraphQL is, why it was created, and what problems it attempts to solve. It is important to understand the underlying reasons for GraphQL's existence as it will help us design better web APIs.
So, what is GraphQL? Let's list some of its main characteristics:
GraphQL provides a standard language for defining data, data types, and related data queries. You can think of GraphQL as roughly analogous to an interface that provides a contract. There's no code there, but you can still see what types and queries are available.
When we create an API using GraphQL, the same GraphQL language will be used to describe our data, its types, and queries no matter what programming language or operating system we use. Having a consistent and reliable representation of data across a wide variety of systems and platforms is, of course, a good thing for clients and systems. But it's also beneficial to programmers, since we can continue to use our normal programming language and frameworks of choice.
In a standard web service, it is the server that controls what fields of the data will be returned. However, in a GraphQL API, it is the client that determines which fields they would like to receive. This gives clients better control and reduces bandwidth usage and cost.
Broadly speaking, there are two main uses of a GraphQL endpoint. One is as a gateway to consolidate other data services, and the other is as the main web API service that directly receives data from a datastore and provides it to clients. Here's a diagram of GraphQL being used as a gateway for other data:
As you can see, GraphQL is acting as the single source of truth for all clients. It works well in this capacity due to its standards-based language that is supported across a wide variety of systems.
For our own application, we will use it as our entire web API, but it is possible to mix it in with existing web services so that GraphQL handles only a part of the service calls being made. This means you do not need to rewrite your entire application. You can introduce GraphQL slowly and deliberately where it makes sense to do so, without disrupting your current application services.
In this section, we took a look at GraphQL at a conceptual level. GraphQL has its own data language, meaning it can be used regardless of server framework, application programming language, or operating system. This flexibility allows GraphQL to be a powerful means of sharing data throughout an organization or even across the web. In the next section, we will explore the GraphQL schema language and see how it works. It will help us structure our data models and understand how to set up our GraphQL server.
As stated, GraphQL is a language used to provide structure and type information to our entity data. Regardless of which vendor's implementation of GraphQL is used on the server, our client can expect the same data structures to be returned. This ability to abstract away the implementation details of servers to clients is one of the strengths of GraphQL.
Let's create a simple GraphQL schema and see what it looks like:
npm init
npm i express apollo-server-express @types/express
tsc -init
Notice, after this command completes, that the default tsconfig.json setting is strict.
import { gql } from "apollo-server-express";
This import gets the gql object, which allows syntax formatting and highlighting of the GraphQL schema language:
const typeDefs = gql`
type User {
id: ID!
username: String!
email: String
}
type Todo {
id: ID!
title: String!
description: String
}
type Query {
getUser(id: ID): User
getTodos: [Todo!]
}
`;
The language is fairly simple and looks a lot like TypeScript. Starting from the top, first we have a User entity, as indicated by the type keyword. type is a GraphQL keyword that indicates that an object of a certain structure is being declared. As you can see, the User type has multiple fields. The id field is of type ID!. The ID type is a built-in type that indicates a unique value, basically a GUID of some kind. The exclamation mark indicates that the field cannot be null, whereas no exclamation mark would indicate that it can be null. Next, we see the username field and its type of String!, which of course means it is a non-nullable string type. Then, we have the description field, but it has a String type without an exclamation mark, so it is nullable.
The Todos type has similar fields, but notice the Query type. This shows that even queries are types in GraphQL. So, if you look at the two queries, getUser and getTodos, you can see why we created the User and Todos types, as they become the return values for our two Query methods. Also notice that the getTodos function returns an array of non-nullable Todos, which is indicated by the brackets. Finally, we export our type definitions using the typeDefs variable:
export default typeDefs;
Type definitions are used by Apollo GraphQL to describe the schema types in a schema file. Before your server can start providing any GraphQL data, it must first have a complete schema file that lists all of your application's types, their fields, and queries that will be served in its API.
Another thing to note is that GraphQL has several default scalar types that are built into the language. These are Int, Float, String, Boolean, and ID. As you noticed in the schema file, we did not have to create a type notation for these types.
In this section, we reviewed what a simple GraphQL schema file looks like. We will be using this syntax as we build out our API. In the next section, we will dive deeper into the GraphQL language and also learn what resolvers are.
In this section we will further explore GraphQL schemas, but we will also implement resolvers, which are the functions that do the actual work. This section will also introduce us to Apollo GraphQL and how to create a GraphQL server instance.
What are resolvers? Resolvers are the functions that get or edit the data from our datastore. This data is then matched with the GraphQL type definition.
In order to see what the role of resolvers is in more depth, we need to continue building out our previous project. Let's look at the steps:
npm i uuid @types/uuid
import express from "express";
import { ApolloServer, makeExecutableSchema } from "apollo-server-express";
import typeDefs from "./typeDefs";
import resolvers from "./resolvers";
Here we import dependencies needed to set up our server. We already created the typeDefs file and we will soon create the resolvers file.
const app = express();
const schema = makeExecutableSchema({ typeDefs, resolvers });
const apolloServer = new ApolloServer({
schema,
context: ({ req, res }: any) => ({ req, res }),
});
apolloServer.applyMiddleware({ app, cors: false });
context is made up of the request and response objects of Express. Then, we add our middleware, which for GraphQL is our Express server object called app. The cors option indicates to disable GraphQL from acting as our CORS server. We'll discuss CORS in later chapters as we build out our app.
In this code, we are now starting up our Express server by listening on port 8000:
app.listen({ port: 8000 }, () => {
console.log("GraphQL server ready.");
});
The listen handler just logs a message to announce it has started.
Now let's create our resolvers:
import { IResolvers } from "apollo-server-express";
import { v4 } from "uuid";
import { GqlContext } from "./GqlContext";
interface User {
id: string;
username: string;
description?: string;
}
interface Todo {
id: string;
title: string;
description?: string;
}
const resolvers: IResolvers = {
Query: {
getUser: async (
obj: any,
args: {
id: string;
},
ctx: GqlContext,
info: any
): Promise<User> => {
return {
id: v4(),
username: "dave",
};
},
Here is our first resolver function, matching the getUser query. Notice that the parameter is more than just the id parameter. This is coming from the Apollo GraphQL server and adds additional information for our call. (Note that to save time, I am hardcoding a User object.) Also, we will create the GqlContext type later, but basically, it is a container that holds our request and response objects that we learned about in Chapter 8, Learning Server-Side Development with Node.js and Express.
getTodos: async (
parent: any,
args: null,
ctx: GqlContext,
info: any
): Promise<Array<Todo>> => {
return [
{
id: v4(),
title: "First todo",
description: "First todo description",
},
{
id: v4(),
title: "Second todo",
description: "Second todo description",
},
{
id: v4(),
title: "Third todo",
},
];
},
},
};
export default resolvers;
As you can see, our actual data getters are just normal TypeScript code. If we had used Java or C# or any other language, the resolvers would simply be Create Read Update Delete (CRUD) operations in those languages as well. The GraphQL server, then, is just translating the data entity models into the types in our type definition schema file.
import { Request, Response } from "express";
export interface GqlContext {
req: Request;
res: Response;
}
This is just a simple shell interface that allows us to provide type safety to our context in our GraphQL resolver calls. As you can see, this type contains the Express Request and Response objects.
Tsc
This will have created js versions of all the ts files.
nodemon server.js
Notice that I have already run one of the queries, which looks like JSON and is on the left, and the result is shown, which is also JSON and on the right. If you look at our query on the left, I am explicitly asking for only the id field, which is why only the id field is returned. Notice that the standard result format is data > <function name> > <fields>. Try running the getTodos query as a test.
As you can see, it looks identical to our typeDefs.ts file.
In this section, we took a look at resolvers by running a small GraphQL server. Resolvers are the other half that makes GraphQL actually function. We also saw how relatively easy it is to get a small GraphQL server running by using the Apollo GraphQL library.
In the next section, we will delve more deeply into queries by looking at mutations and subscriptions.
When creating a GraphQL API, we want to do more than just get data: we may also want to write to a datastore or be notified when some data changes. In this section, we'll see how to do both actions in GraphQL.
Let's take a look at how to write data using mutations first:
import { v4 } from "uuid";
export const todos = [
{
id: v4(),
title: "First todo",
description: "First todo description",
},
{
id: v4(),
title: "Second todo",
description: "Second todo description",
},
{
id: v4(),
title: "Third todo",
},
];
We have just added Todos from our previous list into an array that we are exporting.
import { gql } from "apollo-server-express";
const typeDefs = gql`
type User {
id: ID!
username: String!
email: String
}
type Todo {
id: ID!
title: String!
description: String
}
type Query {
getUser(id: ID): User
getTodos: [Todo!]
}
type Mutation {
addTodo(title: String!, description: String): Todo
}
`;
export default typeDefs;
As you can see, other queries remain the same, but we added a new type called Mutation, which is where any queries that change data will reside. We also added our new mutation called addTodo.
Mutation: {
addTodo: async (
parent: any,
args: {
title: string;
description: string;
},
ctx: GqlContext,
info: any
): Promise<Todo> => {
todos.push({
id: v4(),
title: args.title,
description: args.description
});
return todos[todos.length - 1];
},
},
As you can see, we have a new container object called Mutation, and inside of it is our addTodo mutation. It has similar parameters to the queries, but this mutation will add a new Todo to the todos array. If we run this code in the playground, we see this:
When our query is of type Query, we can leave out the query prefix. However, since this is a mutation, we must include it. As you can see, we only get back id and title, because that is all we asked for.
Now let's take a look at subscriptions, which are a way of being notified when certain data changes. Let's get notified when our addTodo adds a new Todo object:
import express from "express";
import { createServer } from "http";
import {
ApolloServer,
makeExecutableSchema,
PubSub,
} from "apollo-server-express";
import typeDefs from "./typeDefs";
import resolvers from "./resolvers";
First, we get an import of the PubSub type. Notice we also get createServer; we'll use that later.
const app = express();
const pubsub = new PubSub();
const schema = makeExecutableSchema({ typeDefs, resolvers });
const apolloServer = new ApolloServer({
schema,
context: ({ req, res }: any) => ({ req, res, pubsub }),
});
apolloServer.applyMiddleware({ app, cors: false });
const httpServer = createServer(app);
apolloServer.installSubscriptionHandlers(httpServer);
httpServer.listen({ port: 8000 }, () => {
console.log("GraphQL server ready." +
apolloServer.graphqlPath);
console.log("GraphQL subs server ready." +
apolloServer.subscriptionsPath);
});
type Subscription {
newTodo: Todo!
}
import { IResolvers } from "apollo-server-express";
import { v4 } from "uuid";
import { GqlContext } from "./GqlContext";
import { todos } from "./db";
interface User {
id: string;
username: string;
email?: string;
}
interface Todo {
id: string;
title: string;
description?: string;
}
const NEW_TODO = "NEW TODO";
Here we've created a new NEW_TODO constant to act as the name of our new subscription. Subscriptions require a unique label, sort of like a unique key, so that they can be correctly subscribed to and published:
const resolvers: IResolvers = {
Query: {
getUser: async (
parent: any,
args: {
id: string;
},
ctx: GqlContext,
info: any
): Promise<User> => {
return {
id: v4(),
username: "dave",
};
},
As you can see, nothing in our query changes, but it's included here for completeness:
getTodos: async (
parent: any,
args: null,
ctx: GqlContext,
info: any
): Promise<Array<Todo>> => {
return [
{
id: v4(),
title: "First todo",
description: "First todo description",
},
{
id: v4(),
title: "Second todo",
description: "Second todo description",
},
{
id: v4(),
title: "Third todo",
},
];
},
},
Again, our query remains the same:
Mutation: {
addTodo: async (
parent: any,
args: {
title: string;
description: string;
},
{ pubsub }: GqlContext,
Notice that in place of the ctx object, we have deconstructed it to just use the pubsub object, as it's the only one we need:
info: any
): Promise<Todo> => {
const newTodo = {
id: v4(),
title: args.title,
description: args.description,
};
todos.push(newTodo);
pubsub.publish(NEW_TODO, { newTodo });
Here we have publish, which is a function to notify us when we have added a new Todo. Notice the newTodo object is being included in the publish call, so it can be provided to the subscriber later:
return todos[todos.length - 1];
},
},
Subscription: {
newTodo: {
subscribe: (parent, args: null, { pubsub }: GqlContext) =>
pubsub.asyncIterator(NEW_TODO),
Here we subscribe to new Todo adds. Notice that our subscription newTodo is not a function. It's an object with a member called subscribe:
},
},
};
export default resolvers;
The rest is the same as before.
When you click the play button, nothing happens, because a new Todo has not been added yet. So, let's go back to our addTodo tab and add a new Todo. Once you've done that, come back to the newTodo tab and you should see this:
As you can see, that works, and we get the newly added Todo.
In this section, we learned about GraphQL queries, mutations, and subscriptions. We will be using these to build out our application API. Because GraphQL is an industry standard, all GraphQL client frameworks can work with any vendor's GraphQL server framework. Furthermore, clients using a GraphQL API can expect consistent behavior and the same query language regardless of server or vendor. This is the power of GraphQL.
In this chapter, we explored the power and capabilities of GraphQL, one of the hottest new technologies for creating web APIs. GraphQL is an extremely capable technology, but also, because it is an industry standard, we can always expect consistent behavior across servers, frameworks, and languages.
In the next chapter, we will start bringing together the technologies we've learned about thus far and create an Express server using TypeScript, GraphQL, and helper libraries.
3.23.127.197