80 |
Chapter 1: Up and Running with GraphQL and Apollo Server
81 |
82 |
In this chapter, we will:
83 |
84 | - Try running a query against the GitHub GraphQL API
85 | - Use built-in Scalar types, define Object types, and create non-null and list fields
86 | - Set up an Apollo Server with some basic type definitions and corresponding resolvers
87 | - Add arguments to fields
88 | - Run queries against a locally-running GraphQL API using Apollo Studio Explorer
89 |
90 |
91 |
Hello Schema, Nice to Meet You
92 |
The heart of any GraphQL is its schema. While a REST API provides a series of structured routes that can be used to read or write different resources based HTTP verbs, a GraphQL API will instead expose a single endpoint (often at /graphql
) to query any of the data exposed by the API. If you’ve spent many years working with REST APIs, then you may immediately wonder how a single endpoint can do all of the heavy lifting for an API.
93 |
The answer is in the specially formatted query that we send to the GraphQL API endpoint. When sending requests to the API via HTTP, this query is often included in the body of a POST
request. The GraphQL query language allows us to describe exactly what data we want to receive and in what shape we’d like to receive and—just like that—the GraphQL API will send a response back in precisely the format we requested as long as it’s compliant with API’s schema. If you’ve ever felt the pain of simultaneously over-fetching some data from an API while under-fetching the data you really wanted, and then subsequently needing to juggle various asynchronous requests and mash their responses together to finally populate all of the required data for a single view, then GraphQL will feel like a breath of fresh air for you.
94 |
95 |
Convention versus Specification
96 |
The GraphQL specification is a detailed document describing how to define a GraphQL schema, how to write queries against that schema, and how to build an execution engine (also known as a GraphQL server) to respond to those queries. It does not, however, specify anything about the transport mechanism used to shuttle requests to and from a GraphQL server. That said, HTTP is commonly chosen for the transport layer and GraphQL clients often send POST
requests containing a serialized JSON representation of the query in the request body. Similarly, GraphQL servers often respond with JSON as well.
97 |
The POST
verb is a common default for GraphQL requests because GET
requests have size limits and complex queries may grow quite large. Additionally, a POST
request’s body is encrypted over HTTPS, which will help limit potential exposure of any sensitive operation argument values included with the request. That said, tools like Automatic Persisted Queries allow you to hash GraphQL queries to ensure that they fit comfortably within GET
requests limits, and thus allow you to take advantage of browser caches and CDNs.
98 |
99 |
GraphQL’s query language is very intuitive and the best way to learn it is to start writing queries. If you have a GitHub account, you can try sending a query to the GitHub GraphQL API using a browser-based tool available at https://docs.github.com/en/graphql/overview/explorer. After logging in with your GitHub account, the GraphiQL IDE will be active with a viewer
query preloaded in it. Press the play button above the editor to view the response:
100 |

101 |
102 |
With an easy click of a button, you just made your first query to a GraphQL API! And as promised, you were able to query the API for a specific piece of information about the currently logged in-user (in this case, your username), and that data was returned as JSON and in a shape mirroring the original query.
103 |
Now that we’ve seen a query in action, let’s break it down piece by piece:
104 |

105 |
The entirety of what you see above is known as a document in GraphQL speak and that document contains an operation. In this case, it is a query operation that performs a read-only fetch operation, but there are also mutation operations that write and fetch data and there are subscription operations that can fetch data in real-time in response to source events.
106 |
When performing query operations only we can optionally omit the query
keyword in front of the curly brackets as a shorthand:
107 |
108 |
109 |
GraphQL Query
110 |
111 |
{
112 | viewer {
113 | login
114 | }
115 | }
116 |
117 |
118 |
Inside the outermost curly brackets, we are then left with the selection set of fields that specify the precise data that we want to receive in the response. Lastly, note that GraphQL syntax also supports comments by preceding comment text with the #
character.
119 |
We’ve just unpacked several important GraphQL features using this basic query, but some important questions still linger. For one, how would we know that we were able to query a viewer
from the GitHub API in the first place? And how do we find out what other fields can be queried for a viewer
?
120 |
The answer to those questions is the schema—a GraphQL schema defines what types and fields are queryable through the API, as well as the relationships between types. A GraphQL schema also defines the root operation types that it supports, including query
(required), mutation
(optional), and subscription
(optional). We can think of the fields belonging to these root operation types as the entry points to a GraphQL API.
121 |
One popular way to define a GraphQL schema is with the schema definition language (SDL). For example, to support queries for a viewer
field on the query
root operation type, the GitHub API contains the following definition:
122 |
type Query {
123 | viewer: User!
124 | }
125 |
126 |
127 |
Above, the query
root operation type is defined as an Object type called Query
and that type contains a single viewer
field (in reality, the GitHub API’s Query
type contains many more fields, but we’ll start with one to keep things simple). But where does User
come from, and what is its relation to the nested login
field we saw earlier? And what does the exclamation point after User
mean? To answer these questions, we’ll need to dive into GraphQL’s type system.
128 |
Basic Types of Types
129 |
According to the GraphQL specification, types are the “fundamental unit” of a schema. We just saw an example of an Object type when we defined type Query
with its viewer
field above, and there are five other named types available in GraphQL that we can use too.
130 |
Scalar Types
131 |
The most basic type in GraphQL is a Scalar type. We can define custom Scalar types, but we typically start with the ones built into GraphQL. Out of the box, we can use Int
, Float
, String
, Boolean
, and ID
Scalar types in a schema. The first four of those Scalar types represent exactly what they suggest. The ID
type is a special-purpose Scalar type to indicate that a field is a unique identifier and that it will typically not be human-readable. Even if the ID
value is numeric it should always serialize to a string.
132 |
In practice, the built-in Scalar types are used to specify the types that correspond to fields in a schema as follows:
133 |
type User {
134 | bio: String
135 | id: ID
136 | isViewer: Boolean
137 | pinnedItemsRemaining: Int
138 | }
139 |
140 |
141 |
Object Types
142 |
But wait! We haven’t addressed how we were able to arbitrarily define type User
in the last example. We have already seen an example of an Object type when we defined type Query
previously, but we can also define Object types that are specific to our schema.
143 |
Object types are very powerful because they allow us to express relationships (or edges) between the different data types (or nodes) within our graph. If we think of the query
root operation type as an entry point to our API, then Scalar types are the outer nodes in our graph where we finally reach the data. By connecting Object types through their fields, we can facilitate deeper traversal across the graph to these outer nodes.
144 |
In our simplified GitHub API schema, we can move from the root Query
type to a single authenticated User
via the viewer
field. With the following modification, we can go one level deeper:
145 |
type User {
146 | bio: String
147 | id: ID
148 | isViewer: Boolean
149 | pinnedItemsRemaining: Int
150 | status: UserStatus
151 | }
152 |
153 | type UserStatus {
154 | emoji: String
155 | id: ID
156 | message: String
157 | }
158 |
159 |
160 |
Here we see a powerful feature of GraphQL’s type system in action—the ability to define Object types as collections of related fields and then nest them within one another to define the relationships between those types that ultimately allow us to move from node to node across our data graph.
161 |
Much of a GraphQL schema will end up being composed of the Object types that you create, but there are still two specification-defined Object types that we should make note of before moving on. Those types are the Mutation
and Subscription
types that correspond to those root operations in an API. We’ll cover those types in-depth in Chapter 3 and Chapter 10 respectively.
162 |
And if you’re keeping count, you’ve probably noticed that we’ve only explored two of the six named types in GraphQL so far. Scalar and Object types will be all we need to begin building a functioning GraphQL API, so we’ll cover Input types in Chapter 3 and Enum, Interface, and Union types in Chapter 4 as we require them to build out additional API features.
163 |
Two Ways to Modify Types
164 |
By default, GraphQL expects that a field’s corresponding type is singular and that it may return null values. However, in addition to the named types we’ve already explored, the GraphQL specification outlines two different wrapping types that we can use “wrap” named types and modify their defaults.
165 |
Non-Null
166 |
To make a field non-null, we use an exclamation point. Making a field non-null means that a client consuming this API can always expect a value to be returned for this field, and if one is not available, then the query will return an error. We have already seen this where we defined the viewer
field on the Query
type:
167 |
type Query {
168 | viewer: User!
169 | }
170 |
171 |
172 |
As a further example, if we update the User
type to reflect GitHub’s real API, then we would make the id
, isViewer
, and pinnedItemsRemaining
fields non-null:
173 |
type User {
174 | bio: String
175 | id: ID!
176 | isViewer: Boolean!
177 | pinnedItemsRemaining: Int!
178 | status: UserStatus
179 | }
180 |
181 |
182 |
183 |
Best Practice: Think About Nullability Up Front
184 |
Non-null fields have the advantage of making API responses more predictable, but they come with the trade-off of making that API harder to evolve in some ways in the future. For example, if a non-null field is later transitioned to nullable or removed entirely from an Object type, then breaking changes may result for clients that assume the field value will be there.
185 |
The inherently evolvable nature of a GraphQL API combined with the risk of breaking changes where alterations to a field’s nullability are concerned is a good reason to take time to make an informed choice about whether a field should be nullable or non-null when it’s added to a schema.
186 |
When unforeseen circumstances require changes to field nullability in the future, you can use observability tooling to understand what clients are using that field in your API and then proactively help those affected client developers avoid breaking changes to user interfaces.
187 |
188 |
Lists
189 |
Fields can also output lists of types and we indicate such a list by wrapping the type name in brackets. If we wanted to add a non-null field to the User
type that contained a list without any null items, we would update our code as follows:
190 |
type User {
191 | bio: String
192 | id: ID!
193 | isViewer: Boolean!
194 | pinnedItemsRemaining: Int!
195 | status: UserStatus
196 | organizationVerifiedDomainEmails: [String!]!
197 | }
198 |
199 |
200 |
If we wanted to permit a potentially empty list or null items in the list, but still require an empty list to be returned (rather than a null value), then we would specify the field’s output type as [String]!
. Similarly, if we wanted to allow the field itself to be nullable but disallow non-null values in the list, we would specify the field’s output type as [String!]
. Lastly, to permit a nullable list of strings that may contain null values or be empty, we would specify the field’s output type as [String]
.
201 |
A GraphQL API to Call Our Own
202 |
We have now covered just enough GraphQL theory to roll up our sleeves and start building a GraphQL API from scratch with Apollo Server. To do that, we’ll need to define a schema and set up a GraphQL server to respond to client requests to our API.
203 |
Our objective is to build a new and improved version of the REST-based Bibliotech API using GraphQL. Under the hood, the new GraphQL API will use the existing REST endpoints as a data source—a step that many teams take to migrate to GraphQL within an organization. Later on, as the REST API is phased out, different data sources may be swapped in behind the GraphQL API and any clients presently making GraphQL requests will be none the wiser.
204 |
The Bibliotech API provides information about books, authors, and allows users to create a personal library of their favorite books, as well as rate and review them. In the final three chapters, we’ll also build out an MVP client application using React and Apollo Client to consume data from the new GraphQL API. The index page of the Bibliotech React app will look like this:
205 |

206 |
207 |
We’ll leverage the GraphQL API to build out additional pages to display a user’s favorite books, book search results, and a single book view. We will also need to create forms to authenticate users, add and update reviews, and add new authors and books. To begin, let’s create a project directory, cd
into it, and then run the following command to set up the server
directory:
208 |
mkdir server && cd server
209 |
210 |
211 |
Second, we’ll create a package.json
file in the new server
directory (the --yes
flag creates the package.json
file without asking any questions):
212 |
214 |
215 |
Next, we’ll install some initial dependencies:
216 |
npm i apollo-server@2.22.2 concurrently@5.3.0 dotenv@8.2.0 graphql@15.5.0 json-server@0.16.3 node-fetch@2.6.1 nodemon@2.0.7
217 |
218 |
219 |
Let’s take a brief look at each of the packages we just installed:
220 |
221 | apollo-server
: Apollo Server is an open-source GraphQL server that will allow us to define a GraphQL schema and execute queries against that schema.
222 | concurrently
: We can run multiple commands at the same time using this package. It also has support for shortened commands with wildcards.
223 | dotenv
: This package loads environment variables from a .env
file into process.env
.
224 | graphql
: Apollo Server requires this library as a peer dependency.
225 | json-server
: This package allows us to instantly spin up a REST API using data from a JSON file, so we’ll use this to mock the existing Bibliotech API. You may wish to quickly review the JSON Server documentation if you haven’t used it before.
226 | node-fetch
: This is a Node.js implementation of window.fetch
and it will facilitate fetching data from the REST endpoints.
227 | nodemon
: Nodemon will automatically reload our server when files change in the project directory.
228 |
229 |
As an additional piece of configuration in the package.json
file, we’ll set the type
field to module
so that we can use ECMAScript module syntax in our files:
230 |
231 |
232 |
server/package.json
233 |
234 |
{
235 | // ...
236 | "type": "module",
237 | // ...
238 | }
239 |
240 |
241 |
But First, a (Mocked) REST API
242 |
Before we can use the Bibliotech REST API as a backing data source for the GraphQL API, we’ll need to create a file from which JSON Server can query data. Create a db.json
file in the server
directory and add the following code to it:
243 |
244 |
245 |
server/db.json
246 |
247 |
{
248 | "authors": [
249 | {
250 | "id": 1,
251 | "name": "Douglas Adams"
252 | }
253 | ],
254 | "books": [
255 | {
256 | "id": 1,
257 | "cover": "http://covers.openlibrary.org/b/isbn/9780671461492-L.jpg",
258 | "summary": "After a Vogon constructor fleet destroys Earth to make way for a hyperspace bypass, the last surviving man, Arthur Dent, embarks on an interstellar adventure with his friend Ford Prefect (who, apparently, was an alien all along).",
259 | "title": "The Hitchhiker's Guide to the Galaxy"
260 | }
261 | ],
262 | "bookAuthors": [
263 | {
264 | "id": 1,
265 | "authorId": 1,
266 | "bookId": 1
267 | }
268 | ]
269 | }
270 |
271 |
272 |
273 |
If you’re using version control in this project, you may wish to ignore the db.json
file as we will be writing test data to this file as we build out the GraphQL API throughout this book.
274 |
275 |
JSON Server will automatically create an endpoint for each of the top-level properties in the db.json
file. We’ll primarily use the /authors
and /books
endpoints for now, while the /bookAuthors
endpoint will act like a join table would to capture the many-to-many relationship between authors and books. Next, we can make our lives easier by writing some custom routes for the REST API that will make the process of querying for all authors of a given book or all books by a given author a bit more intuitive than querying /bookAuthors
directly. To do this, we’ll create a routes.json
file in the server
directory and add the following code to it:
276 |
277 |
278 |
server/routes.json
279 |
280 |
{
281 | "/authors/:authorId/books": "/bookAuthors?authorId=:authorId&_expand=book",
282 | "/books/:bookId/authors": "/bookAuthors?bookId=:bookId&_expand=author"
283 | }
284 |
285 |
286 |
Lastly, we can remove the existing test
script in the package.json
file and replace it with a script to start up our brand new REST API:
287 |
288 |
289 |
server/package.json
290 |
291 |
{
292 | // ...
293 | "scripts": {
294 | "server:rest": "json-server -w db.json -p 5000 -r routes.json -q"
295 | },
296 | // ...
297 | }
298 |
299 |
300 |
Run npm run server:rest
and try out the following endpoints to confirm they return the expected data before proceeding:
301 |
307 |
Initial Type Definitions
308 |
With our REST API up and running, it’s time to define the first few types in the Bibliotech schema. First, we’ll create a subdirectory to organize our GraphQL-related code, as well as a typeDefs.js
file to house our type definitions. We’ll create a graphql
directory in server
, add the typeDefs.js
file to it, and then set up the new file as follows:
309 |
310 |
311 |
server/src/graphql/typeDefs.js
312 |
313 |
import { gql } from "apollo-server";
314 |
315 | const typeDefs = gql`
316 | # GraphQL type definitions will go here...
317 | `;
318 |
319 | export default typeDefs;
320 |
321 |
322 |
We’re going to define our type definitions as a string inside of a JavaScript file using SDL and then wrap that string in the gql
template literal tag to convert it into the format that Apollo Server is expecting. As a bonus, the gql
tag will enable GraphQL syntax highlighting within the string when the Apollo GraphQL VS Code extension is installed.
323 |
Next, let’s add some initial type definitions for the Author
and Book
Object types, as well as the root Query
type with some related fields for querying lists of authors and books:
324 |
325 |
326 |
server/src/graphql/typeDefs.js
327 |
328 |
import { gql } from "apollo-server";
329 |
330 | const typeDefs = gql`
331 | type Author {
332 | id: ID!
333 | books: [Book]
334 | name: String!
335 | }
336 |
337 | type Book {
338 | id: ID!
339 | authors: [Author]
340 | title: String!
341 | }
342 |
343 | type Query {
344 | authors: [Author]
345 | books: [Book]
346 | }
347 | `;
348 |
349 | export default typeDefs;
350 |
351 |
352 |
Getting lists of authors and books is useful, but we’ll also need a way to query a single author or book based on a unique identifier. To do that, we’ll add some new query fields with arguments. In our previous example with User
Object type from the GitHub API, we mocked the organizationVerifiedDomainEmails
field on the type as it is in the real API, but left out one key detail. This field accepts a login
argument that represents the login of the organization from which to match verified domains. The real field looks like this:
353 |
type User {
354 | # ...
355 | organizationVerifiedDomainEmails(login: String!): [String!]!
356 | }
357 |
358 |
359 |
Parentheses are used after the field name to indicate that the field accepts arguments, and inside, each of the arguments is listed by name with its corresponding type. In this above example, the login
argument expects a non-null String
to be provided when the field is queried.
360 |
For our API, we’ll add author
and book
fields that each accept a non-null ID
as an argument, and return a single Author
or Book
respectively:
361 |
362 |
363 |
server/src/graphql/typeDefs.js
364 |
365 |
import { gql } from "apollo-server";
366 |
367 | const typeDefs = gql`
368 | # ...
369 |
370 | type Query {
371 | author(id: ID!): Author
372 | authors: [Author]
373 | book(id: ID!): Book
374 | books: [Book]
375 | }
376 | `;
377 |
378 | export default typeDefs;
379 |
380 |
381 |
The basic syntax for a query that contains field arguments is as follows:
382 |
383 |
384 |
GraphQL Query
385 |
386 |
query {
387 | author(id: "1") {
388 | name
389 | }
390 | }
391 |
392 |
393 |
Resolvers, aka Functions that Do the Data Fetching
394 |
With some type definitions in place, we now need a way to respond to API requests with real data. To do that, we must write resolver functions. The Apollo Server documentation provides the following concise definition of what a resolver function does:
395 |
396 | A resolver is a function that’s responsible for populating the data for a single field in your schema. It can populate that data in any way you define, such as by fetching data from a back-end database or a third-party API.
397 |
398 |
Resolvers have the following function signature:
399 |
someField(parent, args, context, info) {
400 | return "Hello, world!";
401 | }
402 |
403 |
404 |
Let’s explore what each of the resolver’s parameters exposes in the function:
405 |
406 | parent
: GraphQL resolvers are executed in a chain starting at the top-most level of the query and working down toward the Scalar types at the outer edges. At each step, the field currently being resolved will have access to the data from the previously resolved parent
field in the chain. For the query
, mutation
, and subscription
root operation types, this value will come from the rootValue
function passed to the ApolloServer
constructor. You’ll often see this parameter named to reflect what the parent type is (for example, instead of naming the parameter parent
it will be named book
in field resolvers for the Book
type).
407 | args
: This parameter is an object that contains any arguments supplied for this field as a part of the query. For the example in the previous section, we would expect the args
value of the author
field resolver to be { id: "1" }
.
408 | context
: The context
parameter is another object and it provides us with a way to share data across all of the field resolvers for a given operation. For example, we may wish to include information from a decoded token to authorize access to different fields in our API. The context
object is recreated with each request so we don’t have to worry about the data contained within going stale or inadvertently being shared across requests.
409 | info
: The final info
parameter is typically needed for advanced use cases only and contains information about the field in question, a representation of the entire schema, an abstract syntax tree (AST) for the operation, and more.
410 |
411 |
412 |
Good to Know!
413 |
Resolver functions can return promises, so it’s OK to use the async
keyword with them.
414 |
415 |
We’ll keep our resolvers organized in a separate file, so create a resolvers.js
file in the server/src/graphql
directory. We’ll structure the resolver functions in an object so that Apollo Server will be able to understand what resolvers correspond to which fields. We’ll begin by adding the following code to resolvers.js
:
416 |
417 |
418 |
server/src/graphql/resolvers.js
419 |
420 |
const resolvers = {
421 | Query: {
422 | async author(root, { id }, context, info) {
423 | // Fetch an author by ID here...
424 | },
425 | async authors(root, args, context, info) {
426 | // Fetch all authors here...
427 | },
428 | async book(root, { id }, context, info) {
429 | // Fetch a book by ID here...
430 | },
431 | async books(root, args, context, info) {
432 | // Fetch all books here...
433 | }
434 | }
435 | };
436 |
437 | export default resolvers;
438 |
439 |
440 |
Note that each key nested in the object value of the Query
key directly corresponds to a field name from the type definitions we just created. We have also marked each function as async
because we must await
a GET
request to the mocked REST API to obtain the data that will be returned for the fields. Let’s update each resolver to fetch the appropriate data now:
441 |
442 |
443 |
server/src/graphql/resolvers.js
444 |
445 |
import fetch from "node-fetch";
446 |
447 | const baseURL = "http://localhost:5000";
448 |
449 | const resolvers = {
450 | Query: {
451 | async author(root, { id }, context, info) {
452 | const res = await fetch(`${baseURL}/authors/${id}`).catch(
453 | err => err.message === "404: Not Found" && null
454 | );
455 | return res.json();
456 | },
457 | async authors(root, args, context, info) {
458 | const res = await fetch(`${baseURL}/authors`);
459 | return res.json();
460 | },
461 | async book(root, { id }, context, info) {
462 | const res = await fetch(`${baseURL}/books/${id}`).catch(
463 | err => err.message === "404: Not Found" && null
464 | );
465 | return res.json();
466 | },
467 | async books(root, args, context, info) {
468 | const res = await fetch(`${baseURL}/books`);
469 | return res.json();
470 | }
471 | }
472 | };
473 |
474 | export default resolvers;
475 |
476 |
477 |
This code is all we need to take care of resolving data for fields on the Query
type, but what about the Author
and Book
types? As you can imagine, if a schema contained many Object types with many different fields, then it could get very tedious to write resolvers for each field.
478 |
Luckily, if the parent
object argument contains a property with a name that matches the field name, then Apollo Server will use a default resolver to automatically supply that value for the field. That means that id
and name
fields will be automatically resolved for the Author
type and the id
and title
fields will be automatically resolved for the Book
type because these properties are readily available in the objects fetched from the REST API.
479 |
However, because we expressed a many-to-many relationship between authors and books in our schema, we will need to provide a books
field resolver for Author
and an authors
field resolver for Book
. To do that, we’ll update our code:
480 |
481 |
482 |
server/src/graphql/resolvers.js
483 |
484 |
// ...
485 |
486 | const resolvers = {
487 | Author: {
488 | async books(author, args, context, info) {
489 | const res = await fetch(`${baseURL}/authors/${author.id}/books`);
490 | const items = await res.json();
491 | return items.map(item => item.book);
492 | }
493 | },
494 | Book: {
495 | async authors(book, args, context, info) {
496 | const res = await fetch(`${baseURL}/books/${book.id}/authors`);
497 | const items = await res.json();
498 | return items.map(item => item.author);
499 | }
500 | },
501 | Query: {
502 | // ...
503 | }
504 | };
505 |
506 | export default resolvers;
507 |
508 |
509 |
We now have all of the required code in place to resolve every field in the schema.
510 |
Wire Up Apollo Server
511 |
Now that we have type definitions and resolvers ready to go, we’re finally ready to create a new Apollo Server and start sending requests to the GraphQL API. We’ll instantiate an ApolloServer
object in a top-level index.js
file for our server application. Inside the server/src
directory, create the index.js
file first. To create an ApolloServer
, we only need to pass in our typeDefs
and resolvers
and call its listen
method to start it up (under the hood, Apollo Server runs on Express by default). We can do that in about a dozen lines of code in the new index.js
file:
512 |
513 |
514 |
server/src/index.js
515 |
516 |
import { ApolloServer } from "apollo-server";
517 |
518 | import resolvers from "./graphql/resolvers.js";
519 | import typeDefs from "./graphql/typeDefs.js";
520 |
521 | const server = new ApolloServer({
522 | typeDefs,
523 | resolvers
524 | });
525 |
526 | server.listen().then(({ url }) => {
527 | console.log(`Server ready at ${url}`);
528 | });
529 |
530 |
531 |
Just one last step! We’ll need a server:graphql
script to start up the GraphQL API. In this script, we’ll set the --ignore
option for Nodemon because we don’t need to restart the server every time a record is added, updated, or removed from db.json
. For convenience, we’ll use Concurrently to create a top-level server
script to start the REST API and GraphQL API together:
532 |
533 |
534 |
server/package.json
535 |
536 |
{
537 | // ...
538 | "scripts": {
539 | "server": "concurrently -k npm:server:*",
540 | "server:rest": "json-server -w db.json -p 5000 -r routes.json -q",
541 | "server:graphql": "nodemon --ignore db.json ./src/index.js"
542 | },
543 | // ...
544 | }
545 |
546 |
547 |
If we try running npm run server
in the terminal now, then we should see both APIs start up. By default, the GraphQL API will be available at http://localhost:4000/ and we will access it at the /graphql
path. We can take the GraphQL API for a quick test spin by opening another terminal tab or window and running this cURL command:
548 |
curl 'http://localhost:4000/graphql' -H 'Content-Type: application/json' -H 'Accept: application/json' --data-binary '{"query":"query { books { title id } }"}' --compressed
549 |
550 |
551 |
The following response output should appear in the terminal:
552 |
{"data":{"books":[{"title":"The Hitchhiker's Guide to the Galaxy","id":"1"}]}}
553 |
554 |
555 |
Congratulations! You are now the proud owner of a shiny, new GraphQL API.
556 |
Exploring the GraphQL API
557 |
Now that our API is up and running, it would be easier if we had a more visual way to experiment with it than cURL commands. There are many browser-based GraphQL IDEs to choose from today—we’ve already seen one such example when we used the embedded GraphiQL tool to try out the GitHub API. GraphQL Playground is another option, and it even comes bundled with Apollo Server. If you navigate to http://localhost:4000/graphql directly in your browser, you can explore your GraphQL API and run queries against it using GraphQL Playground right now.
558 |
Currently, one of the most feature-rich, browser-based IDEs designed for testing GraphQL operations is Apollo Studio Explorer. It includes a clickable query builder interface, operation history, and a spotlight-style search for browsing types and fields. Explorer is offered as a free feature of the Apollo Studio platform and examples throughout this book will demonstrate how to use it as a GraphQL IDE.
559 |
There are two different ways that you can access Explorer. If you would prefer to use it without creating an Apollo Studio account, you can navigate to https://sandbox.apollo.dev and update the sandbox URL to http://localhost:4000/graphql and you will be able to explore the Bibliotech GraphQL API immediately (skip to Page 21 for further directions on how to do so).
560 |
Alternatively, for a more feature-rich experience using Explorer (including a query history and additional customization settings), you can sign up for an Apollo Studio account by following these quick steps. First, navigate to https://studio.apollographql.com/ to register for a new account:
561 |

562 |
563 |
Click the “Create an account” link and then create a new account either using your email or GitHub account. Be sure to pick the “Local development” plan. Once your account has been created, you’ll have the option to choose a “Deployed” or “Development” graph. A deployed graph can take advantage of Apollo Studio features such as its schema registry, metrics reporting, and is typically meant for team collaboration. A development graph will poll your development server for schema updates as you work. For our purposes, we will choose a development graph. Be sure to give it a unique name and set the correct endpoint:
564 |

565 |
566 |
After creating your graph, Explorer will load with a demo query operation in the editor:
567 |

568 |
569 |
The auto update option will keep Explorer in sync with the schema in your local development environment.
570 |
Next, replace the demo query in the editor with the following operation document:
571 |
query {
572 | authors {
573 | id
574 | }
575 | }
576 |
577 |
578 |
Then click on authors: [Author]
under the “Fields” subheading in the lefthand panel. Click the plus sign next to the name
and books
fields to automatically add them to the query (you can also type the field names into the editor if you prefer). Click and add more fields under the books
field as well, and then press the “Run” button above the editor to execute the query:
579 |

580 |
581 |
In the image above, the table view has been toggled to display the results in a table format instead of the default JSON format. Keep this feature in mind when viewing complex lists of results. Before you move on to the next chapter, be sure to try out some of Explorer’s other features, such as the spotlight-style search (accessible from the magnifying glass icon at the top of the lefthand panel). And if you signed up for an Apollo Studio account, try out some of the various settings you can configure (from the gear icon) such as dark mode and editor hints.
582 |
As a final feature to highlight, Apollo Studio also provides a convenient interface for exploring the different types in your schema. As schemas grow in size, it can become increasingly difficult to have a bird’s eye view of all of the types in it, especially if all of those type definitions are contained within a single file. You can access an overview of all of your type definitions organized by type by navigating to the “Schema” page (it’s the first item in the lefthand navigation menu):
583 |

584 |
585 |
586 |
Introspection: The GraphQL Magic that Makes Tools Like Explorer Possible
587 |
You may wonder how it’s possible for tools such as Explorer to know so much about your GraphQL schema simply by sharing your API’s endpoint. A GraphQL schema can share information about itself via introspection. In practice, that means that a GraphQL API exposes a special __schema
field on the query
root operation type that allows us to request information about the API itself, such as the names of its various types and fields.
588 |
You can see introspection in action by running the following query:
589 |
{
590 | __schema {
591 | types {
592 | name
593 | fields {
594 | name
595 | }
596 | }
597 | }
598 | }
599 |
600 |
601 |
Note that it’s considered a best practice to turn off introspection in production for non-public GraphQL APIs to prevent potential bad actors from learning about the detailed inner workings of the API by sending an introspection query to it.
602 |
603 |
Summary
604 |
We covered a lot of ground in this chapter, from learning how to write basic queries against a third-party GraphQL API, to creating type definitions for a schema of our own, and writing resolver functions to provide data for all of the fields in our API. We also learned about two named types in the GraphQL type system—Object and Scalar types—and learned how to add arguments to fields. Lastly, we saw how a visual tool like Apollo Studio Explorer can make it easy to experiment with a GraphQL API as we develop it.
605 |
In the next chapter, we’ll continue to add new Object types and additional query
fields to the Bibliotech API while also adopting some best practices related to GraphQL API design and development.
606 |
614 |
617 |