0% found this document useful (0 votes)
6 views28 pages

Understanding The List API in GraphQL With Examples

Uploaded by

ashwitha
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views28 pages

Understanding The List API in GraphQL With Examples

Uploaded by

ashwitha
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

Understanding the List API in GraphQL with

Examples

To begin building the Issue Tracker application using GraphQL, we


now focus on creating a List API. This will allow us to retrieve a list
of issues through a GraphQL query. The process involves:
 schema definition,
 server-side resolver implementation, and finally,
 testing the query using the GraphQL Playground.

Sample code :
const { ApolloServer, gql } = require('apollo-server');

// In-memory data
let aboutMessage = "API for tracking issues";

const issues = [
{ id: 1, title: "Fix login bug", status: "Open", owner: "Alice", effort: 3, created: "2024-01-
10", due: "2024-01-15" },
{ id: 2, title: "Improve UI", status: "Assigned", owner: "Bob", effort: 5, created: "2024-01-
12", due: "2024-01-20" },
];

// Define the GraphQL schema


const typeDefs = gql`
type Issue {
id: Int!
title: String!
status: String!
owner: String
effort: Int
created: String!
due: String
}

type Query {
about: String!
issueList: [Issue!]!
}

type Mutation {
setAboutMessage(message: String!): String
}
`;

// Resolver functions
const resolvers = {
Query: {
about: () => aboutMessage,
issueList: () => issues,
},
Mutation: {
setAboutMessage: (_, { message }) => {
aboutMessage = message;
return aboutMessage;
},
},
};

// Create Apollo Server


const server = new ApolloServer({ typeDefs, resolvers });

// Start server
[Link]().then(({ url }) => {
[Link](`🚀 Server ready at ${url}`);
});

1. Defining the Custom Type: Issue


In GraphQL, we define custom types to structure the data returned by
the API. Here, we define a type called Issue that represents the
structure of an issue object. GraphQL does not have a built-in Date
type, so for fields like created and due, we use String for now. Here's what
the type looks like:
type Issue {
id: Int!
title: String!
status: String!
owner: String
effort: Int
created: String!
due: String
}
Explanation:
 id, title, status, and created are marked with an exclamation mark ( !)
meaning they are required and cannot be null.
 owner, effort, and due are optional, which means they can be null or
omitted.
 We use String for dates like created and due, since we'll convert
actual JavaScript Date objects to strings when sending data.
2. Adding the Query to Return a List of Issues
Next, we define a new field in the Query type called issueList. This field
returns an array of Issue objects. The notation [Issue!]! ensures that:
 The list itself will never be null.
 None of the items in the list will be null.
Here’s how the Query and Mutation declarations look:
##### Top level declarations
type Query { //Read API(fetch data)
about: String!
issueList: [Issue!]!
}

type Mutation { // Write API(change data)


setAboutMessage(message: String!): String
}
This schema tells GraphQL that when a client queries issueList, it
should expect a non-nullable array of non-nullable Issue objects.

3. Server-side: Resolver and Mock Database


Now we move to the server-side code. First, we create a sample
database using a plain JavaScript array named issuesDB. It contains two
sample issues:
const issuesDB = [
{
id: 1,
status: 'New',
owner: 'Ravan',
effort: 5,
created: new Date('2019-01-15'),
due: undefined,
title: 'Error in console when clicking Add',
},
{
id: 2,
status: 'Assigned',
owner: 'Eddie',
effort: 14,
created: new Date('2019-01-16'),
due: new Date('2019-02-01'),
title: 'Missing bottom border on panel',
},
];
This array acts as a placeholder for a database. Each object matches
the structure of the Issue type defined in the GraphQL schema.
We then write a resolver function named issueList that simply returns
this array when called:
function issueList() {
return issuesDB;
}
We connect this function to the GraphQL Query using the resolvers
object:
const resolvers = {
Query: {
about: () => aboutMessage,
issueList,
},
Mutation: {
setAboutMessage,
},
};

// src/[Link]
import React, { useEffect, useState } from 'react';
import './[Link]';
function IssueList() {
const [issues, setIssues] = useState([]);
useEffect(() => {
async function loadData() {
const query = `query {
issueList {
id title status owner
created effort due
}
}`;
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({ query }),
});
const result = await [Link]();
setIssues([Link]);
}
loadData();
}, []); // [] ensures this runs only once when component mounts

return (
<div>
<h1>Issue Tracker</h1>
<table border="1">
<thead>
<tr>
<th>ID</th><th>Title</th><th>Status</th><th>Owner</th><th>Cre
ated</th><th>Due</th><th>Effort</th>
</tr>
</thead>
<tbody>
{[Link](issue => (
<tr key={[Link]}>
<td>{[Link]}</td>
<td>{[Link]}</td>
<td>{[Link]}</td>
<td>{[Link]}</td>
<td>{[Link]}</td>
<td>{[Link]}</td>
<td>{[Link]}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function App() {
return (
<div className="App">
<IssueList />
</div>
);
}
export default App;
4. Running a Query in GraphQL Playground
Once everything is in place, we can test the issueList query in the
GraphQL Playground. It is important to refresh the browser to load
the latest schema so that the playground understands the new issueList
field.
Here’s a sample query:
query {
issueList {
id
title
created
}
}
This query asks for the list of issues, but only requests three fields for
each issue: id, title, and created.
5. Sample Output Returned by the API
The result of the above query will look like this:
{
"data": {
"issueList": [
{
"id": 1,
"title": "Error in console when clicking Add",
"created": "Tue Jan 15 2019 [Link] GMT+0530 (India Standard Time)"
},
{
"id": 2,
"title": "Missing bottom border on panel",
"created": "Wed Jan 16 2019 [Link] GMT+0530 (India Standard Time)"
}
]
}
}
The created field in each issue is displayed as a formatted date string.
This happens because in JavaScript, when a Date object is returned as
part of an API response, it is automatically converted to a string using
the toString() method.
If we wanted to see more fields in the response, we could add them in
the query. For example:
query {
issueList {
id
title
owner
effort
status
created
due
}
}
This would return a more complete issue object in the response.
Summary
To summarize, in this section we created a new GraphQL type Issue,
updated the schema with a new field issueList that returns a non-
nullable list of Issue objects, implemented a simple resolver on the
server that fetches this list from a static array, and finally tested it
using GraphQL Playground. Through this setup, we not only
practiced GraphQL’s schema design but also understood how to
structure APIs for real-world features like listing issues in an issue
tracking app.

The List API Integration

Now that the List API is ready and working on the backend, the next
step is to connect it to the user interface. This means updating the
React frontend so that it fetches the list of issues directly from the
server, instead of using hardcoded data. We will do this by changing
the loadData() method inside the IssueList React component.
To fetch data from the server, we need to make asynchronous API
calls. This is often called an Ajax call. Earlier, developers used the
$.ajax() function from the jQuery library for this purpose, but using
jQuery just for Ajax feels like too much because the library itself is
large. Fortunately, most modern browsers support a built-in method
called the Fetch API that lets us make Ajax calls easily. However, older
browsers like Internet Explorer don’t support it. For those, we can use
a “polyfill,” which is like a patch that adds support for the fetch
function. One such polyfill is called whatwg-fetch. We can include this
polyfill in our project by adding a <script> tag in the [Link] file, using
the CDN [Link].
Here is the change you will make in [Link]:
<script src="[Link]
<script src="[Link]
This polyfill is only needed for older browsers. New browsers like
Chrome, Firefox, Safari, Edge, and Opera already support fetch()
without any extra code.
Now we move to updating the loadData() method. Inside this method,
we will write a GraphQL query as a string. This query looks very
similar to what we typed earlier in the GraphQL Playground when
testing the issueList API. Since we want to fetch all fields from each
issue, the query will include all of them. Here’s how the query string
will look:
const query = `query {
issueList {
id title status owner
created effort due
}
}`;
We will send this query to the server using the fetch() function. It will
be a POST request, and we’ll need to include headers to tell the server
that we are sending JSON data. We’ll also include the query string in
the body of the request. Here’s the complete code to send the request:
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json'},
body: [Link]({ query })
});
Once the request is made, the server will send a response. We need to
convert this response from JSON format into a regular JavaScript
object. This can be done by calling [Link](). Once we have the
result, we update the React component’s state using [Link](). This
sets the list of issues that came from the server into the component’s
issues variable. The final part of the method looks like this:
const result = await [Link]();
[Link]({ issues: [Link] });
Since we are using the await keyword inside loadData(), we must also
mark the function as async. Otherwise, JavaScript will throw an error.
The beginning of the function will now be written as async loadData().
After making these changes, if you refresh the Issue Tracker
application in the browser, you will probably see an error. This
happens because the created and due fields are now plain strings, but in
the IssueRow component we are calling the .toDateString() function, which
only works on real JavaScript Date objects. So, trying to call
.toDateString() on a string causes an error.
To fix this, we just need to stop converting the dates and show them
directly. In the IssueRow component, we replace the lines:
<td>{[Link]()}</td>
<td>{[Link] ? [Link]() : ''}</td>
with:
<td>{[Link]}</td>
<td>{[Link]}</td>
This way, we avoid calling .toDateString() and just print the raw string
that came from the server.
Because we are now loading issues from the server, we no longer
need the old global variable initialIssues. That variable can be removed
from the code. Earlier, initialIssues looked like this:
const initialIssues = [
{
id: 1, status: 'New', owner: 'Ravan', effort: 5,
created: new Date('2018-08-15'), due: undefined,
title: 'Error in console when clicking Add',
},
{
id: 2, status: 'Assigned', owner: 'Eddie', effort: 14,
created: new Date('2018-08-16'), due: new Date('2018-08-30'),
title: 'Missing bottom border on panel',
},
];
It was being used in loadData() like this:
setTimeout(() => {
[Link]({ issues: initialIssues });
}, 500);
Now, this part is no longer needed, because we are getting the issues
from the server using fetch. So the new version of loadData() becomes:
async loadData() {
const query = `query {
issueList {
id title status owner
created effort due
}
}`;
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json'},
body: [Link]({ query })
});
const result = await [Link]();
[Link]({ issues: [Link] });
}
Once these changes are done, if you refresh your browser, you will
see that the application now displays the list of issues fetched from
the server. The dates will appear as long strings like “Tue Jan 15 2019
[Link] GMT+0530 (India Standard Time)”, because they are raw
string formats. But otherwise, the application looks exactly like
before. The "Add" operation will not work correctly yet, because it is
still sending real Date objects when adding new issues, while our
server is expecting and returning strings. We will fix that in the next
section.
This completes the integration of the List API into the frontend of the
Issue Tracker application.

The Create API:


When this API is called, the new issue will be added to the existing
list of issues stored in memory on the server.
The first thing we need to do is update the GraphQL schema.
Specifically, we need to add a new field under the Mutation type, called
issueAdd. This field will take input values such as title, owner, effort,
and due date for the new issue. However, instead of sending each of
these values as separate arguments, we can create a new input type
that combines all the needed fields into one object. This is better and
cleaner, especially since some fields like id and created are not part of
the input — they are generated on the server side. Also, GraphQL
requires input types to be defined using the input keyword rather than
type.
We’ll call this input type IssueInputs. This type includes all the
necessary fields to create a new issue, except id and created. These will
be set by the server automatically. Here’s how the new input type is
added in the schema:
"Toned down Issue, used as inputs, without server generated values."
input IssueInputs {
title: String!
"Optional, if not supplied, will be set to 'New'"
status: String
owner: String
effort: Int
due: GraphQLDate
}
The lines in double quotes above each field are special descriptions
that show up in schema explorer tools like the GraphQL Playground.
These descriptions act like tooltips or documentation to help
developers understand what each field means.
Now that we’ve defined the input type, we can add the new field
issueAdd to the Mutation type. This field will accept an argument of type
IssueInputs! (non-nullable), and it will return an Issue! object, which
means the full issue including the generated fields id and created. Here
is the final version of the schema after making all these changes:
##### Top level declarations
type Mutation {
setAboutMessage(message: String!): String
issueAdd(issue: IssueInputs!): Issue!
}
With the schema updated, the next step is to write a resolver function
for the issueAdd mutation. This function will receive the issue object
from the GraphQL request and do a few things with it: it will assign a
new id to the issue, set the current date as the created value, default the
status to 'New' if it wasn’t given, and then finally push the issue into
the in-memory array of issues.
Here’s how the issueAdd function is written:
function issueAdd(_, { issue }) {
[Link] = new Date();
[Link] = [Link] + 1;
if ([Link] == undefined) [Link] = 'New';
[Link](issue);
return issue;
}
Notice that we used JavaScript’s new Date() to set the current
timestamp, and we used [Link] + 1 to assign a unique ID. If the
status was not provided in the input, we set it to 'New' by default.
Finally, we pushed the new issue into the existing issuesDB array and
returned it.
Now, we register this function in the Mutation part of the resolvers object,
like this:
Mutation: {
setAboutMessage,
issueAdd,
},
At this point, you might notice that the schema is now using a new
type called GraphQLDate. This is a custom scalar type that we defined
earlier, but until now we didn’t need to implement the parser logic for
it. Since our IssueInputs now includes the due field as a GraphQLDate, we
must now write the parser methods for it. This includes two methods:
parseValue and parseLiteral.
The parseValue method is used when the input is supplied as a variable.
It receives the value directly and converts it into a Date object. Here’s
the code for it:
parseValue(value) {
return new Date(value);
},
The parseLiteral method is used when the input is typed directly in the
GraphQL query. It receives an object called ast (abstract syntax tree).
This object has a kind property, which tells us what type of value was
found in the query. For GraphQLDate, we only support strings. So we
check if the kind is [Link], and if it is, we convert the string to a
date and return it. If not, we return undefined. Here’s the code:
parseLiteral(ast) {
return ([Link] == [Link]) ? new Date([Link]) : undefined;
},
Together, these methods form the parser logic for GraphQLDate, which
we then register in the server as follows:
const GraphQLDate = new GraphQLScalarType({
...
parseValue(value) {
return new Date(value);
},
parseLiteral(ast) {
return ([Link] == [Link]) ? new Date([Link]) : undefined;
},
});
We now include GraphQLDate in the resolvers list like this:
const resolvers = {
...
Mutation: {
setAboutMessage,
issueAdd,
},
GraphQLDate,
};
All of these changes together complete the backend implementation
for the Create API.
We can now test the new issueAdd mutation using the GraphQL
Playground. After refreshing the browser so that it loads the updated
schema, you will be able to explore the schema and see the field
descriptions we wrote earlier. This includes the one that says the status
will default to 'New' if not provided.
To add a new issue, you can write the following mutation in the
Playground:
mutation {
issueAdd(issue:{
title: "Completion date should be optional",
owner: "Pieta",
due: "2018-12-13",
}) {
id
due
created
status
}
}
When you run this mutation, you should get a response like this:
{
"data": {
"issueAdd": {
"id": 4,
"due": "2018-12-13T[Link].000Z",
"created": "2018-10-03T[Link].551Z",
"status": "New"
}
}
}
This shows that the new issue has been added successfully. The server
generated an ID of 4 and set the created field to the current date and
time. It also converted the due string into a proper date format. Since
the status was not supplied, it was correctly defaulted to 'New'.
You can confirm that the new issue has really been added by running
the issueList query again in the Playground. The newly created issue
will appear along with the previous ones.

Create API Integration

We now integrate the Create API into the UI of the Issue Tracker
application. The first step in this process is to slightly modify how
new issues are defined in the IssueAdd component. Previously, the
server set the default status to 'New', but now we’ll explicitly include
that value in the frontend. We will also automatically set the due date
to be ten days from the current date. This logic is added in the
handleSubmit() method inside the IssueAdd component in [Link]. The
updated function includes this part:
const issue = {
owner: [Link],
title: [Link],
status: 'New',
due: new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 10),
};
Here, we are using [Link] and [Link]
to get user input
values from the form fields. We also calculate the due date by adding
ten days (in milliseconds) to the current timestamp.
Once the new issue object is created, we send it to the createIssue()
method of the parent component through [Link](issue);. This
is how the handleSubmit() method ends:
[Link](issue);
[Link] = ""; [Link] = "";
}
Now we move on to the createIssue() method inside the IssueList
component. This method is responsible for sending the new issue to
the backend server using a GraphQL mutation. To do that, we create a
GraphQL mutation string using a template literal. In the mutation, we
must convert the due date to a proper ISO string format using the
.toISOString() method. This ensures that the backend parses it correctly
using the custom scalar type GraphQLDate. The mutation string looks
like this:
const query = `mutation {
issueAdd(issue:{
title: "${[Link]}",
owner: "${[Link]}",
due: "${[Link]()}"
}) {
id
}
}`;
After forming the query string, we use fetch() to send this request to the
server. We use a POST request with headers that specify the content
type as JSON, and we include the query inside the request body:
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({ query })
});
Rather than manually adding the new issue to the state, we use a safer
approach: we call [Link]() after the new issue is submitted. This
ensures that we always fetch the latest list of issues from the server,
even if another user added an issue or if something went wrong
during the process. Here’s the final part of the createIssue() method:
[Link]();
}
Now we put all the relevant parts together to see the complete and
correct version of the changes in [Link], as shown in Listing 5-14
from the textbook.
class IssueAdd extends [Link] {
constructor() {
super();
[Link] = [Link](this);
}

handleSubmit(e) {
[Link]();
const form = [Link];
const issue = {
owner: [Link],
title: [Link],
status: 'New',
due: new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 10),
};
[Link](issue);
[Link] = ""; [Link] = "";
}

render() {
return (
<form name="issueAdd" onSubmit={[Link]}>
<input type="text" name="owner" placeholder="Owner" />
<input type="text" name="title" placeholder="Title" />
<button>Add</button>
</form>
);
}
}
The createIssue() method inside the IssueList component is updated as
follows:
async createIssue(issue) {
const query = `mutation {
issueAdd(issue:{
title: "${[Link]}",
owner: "${[Link]}",
due: "${[Link]()}"
}) {
id
}
}`;

const response = await fetch('/graphql', {


method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({ query })
});

[Link]();
}
Once this integration is complete, you can test it by running the
application in the browser. When you add a new issue using the form,
the due date will automatically be set to ten days from today. You can
confirm this by submitting the form and checking that the new issue
appears in the list. If you refresh the browser, the issue remains
because it is now saved on the server and fetched again with the
loadData() method.

Query variables:

Until now, whenever we sent data to the server—for example, in the issueAdd
mutation—we wrote the user’s input values directly inside the GraphQL query
string. This worked fine for simple testing or fixed values. We even used
template strings in JavaScript to include user input into the GraphQL query
dynamically. However, this method is not very reliable. There are two main
reasons for this.

First, every time we use template strings to build the query, there is some small
processing overhead as the string is evaluated and converted into the actual
query. But the more serious issue is that string formatting like this becomes
risky when user inputs include special characters such as quotation marks (") or
curly braces ({}). These characters have meaning inside strings and can break
the query if not escaped properly. If you test the current Issue Tracker app and
try to add an issue where the title includes a double quote—like Error in
"Submit" button—you’ll find that it causes problems.

GraphQL solves this problem using a clean and safe feature called variables.
Just like prepared statements in SQL, GraphQL allows you to define the
structure of a query separately and pass the actual values (variables) in a
separate object. This keeps the query cleaner and prevents injection errors or
syntax problems.

To use variables in a GraphQL query, we must follow three steps. First, we give
a name to the operation. For example, if we want to name our mutation for
setting the about message, we write:

mutation setNewMessage {
setAboutMessage(message: "New About Message")
}
Then, we replace the hardcoded value in the query with a variable. GraphQL
variables always start with the $ symbol. So instead of the message string, we
now write $message in the query. But to make this work, we must declare that
$message is a variable of type String! (non-nullable string). The query becomes:

mutation setNewMessage($message: String!) {


setAboutMessage(message: $message)
}
Finally, we pass the actual value of the variable in a separate JSON object. If we
are using the GraphQL Playground, you will see a section at the bottom-right
called QUERY VARIABLES. When you click on it, it opens up a second
window below the query editor. You can write the variables there like this:

{
"message": "Hello World!"
}
Behind the scenes, GraphQL combines all of this into a single JSON request
with three keys: query, operationName, and variables.

Now, coming back to our actual application—the Issue Tracker—we will apply
the same idea to the issueAdd mutation. Earlier, we were building a template
string like this:

const query = `mutation {


issueAdd(issue:{
title: "${[Link]}",
owner: "${[Link]}",
due: "${[Link]()}",
}) {
id
}
}`;
Now we can replace this with a much safer and cleaner approach using
variables. We start by naming the mutation issueAdd, and we define a variable
called $issue, which is of type IssueInputs!. This type was already created in an
earlier chapter. The new query string becomes:

const query = `mutation issueAdd($issue: IssueInputs!) {


issueAdd(issue: $issue) {
id
}
}`;
Notice how the issueAdd field now accepts $issue as a variable, and we no
longer need to manually insert strings inside the query.

Next, when we make the fetch call, we pass the variables property along with
the query in the request body. The variables object contains the issue object, and
the issue key must match the variable name used in the query (without the $
sign). So the updated fetch() request is written like this:

const response = await fetch('/graphql', {


method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({ query, variables: { issue } })
});
This completely eliminates the risk of breaking the query string when special
characters are entered by the user.

Now let’s look at the complete and updated version of the createIssue() method
inside the IssueList component, as shown in Listing 5-15:

async createIssue(issue) {
const query = `mutation issueAdd($issue: IssueInputs!) {
issueAdd(issue: $issue) {
id
}
}`;

const response = await fetch('/graphql', {


method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({ query, variables: { issue } })
});

[Link]();
}
With these changes, the application works just as before when you test it by
adding a new issue. But now, it is much safer and more reliable. You can now
add special characters like double quotes in the title, and the mutation will still
work perfectly without throwing an error.

Input Validations:

So far, we have not added any validations to the Issue Tracker application. But
in most real-world applications, input validation is essential — not just to block
wrong inputs from the UI, but also to stop invalid data from being sent directly
to the backend through APIs. In this section, we will implement two types of
validation: one handled by the GraphQL schema itself, and another handled
programmatically using custom logic on the server.

One common form of validation is to restrict the allowed values for a field. For
example, the status field in our app should only accept certain values like 'New',
'Assigned', 'Fixed', or 'Closed'. While we could perform this check inside the
server logic, GraphQL provides a cleaner way using enums, or enumeration
types. An enum is a type that lists all allowed values for a field. For example,
this is how we can define a simple enum in GraphQL:

enum Color {
Red
Green
Blue
}
Although many languages treat enums as special data types, JavaScript treats
enum values simply as strings. So both on the client and server sides, these
values will still be handled as strings, but the schema will enforce their
correctness.

We now define an enum in our schema specifically for issue status. This enum
is called StatusType and looks like this:
enum StatusType {
New
Assigned
Fixed
Closed
}
Next, we update the status field in the Issue type to use this enum instead of
String. So the updated part of the Issue type becomes:

type Issue {
...
status: StatusType!
...
}
We make the same update in the IssueInputs input type. Additionally, GraphQL
allows us to specify default values directly in the schema. This is useful for
fields that are optional but should still receive a value if none is provided. In our
case, we want the status field to default to New if it's not given. Here’s how we
write that in the input type:

input IssueInputs {
...
status: StatusType = New
owner: String
effort: Int
due: GraphQLDate
}
Now that the schema will default the status to 'New', we no longer need to set
that default manually in the issueAdd resolver function in [Link]. That line
can be removed.

Next, we implement programmatic validation. These are checks that happen in


code before the issue is saved. For this, we create a new function called
validateIssue() in [Link]. This function will collect all validation errors in an
array called errors. If any validations fail, we throw a special error using the
UserInputError class from Apollo Server.

Here is how we define the function:

function validateIssue(_, { issue }) {


const errors = [];
if ([Link] < 3) {
[Link]('Field "title" must be at least 3 characters long.');
}
if ([Link] == 'Assigned' && ![Link]) {
[Link]('Field "owner" is required when status is "Assigned".');
}
if ([Link] > 0) {
throw new UserInputError('Invalid input(s)', { errors });
}
}
In this function, we first check if the title is too short. If it's less than 3
characters, we add a message. Next, we add a conditional rule: if the status is
'Assigned', then the owner field must be provided. This is a good example of a
conditional validation. Finally, if there are any errors in the errors array, we
throw a UserInputError, which will be returned to the client.

Now we modify the issueAdd resolver to include this validation call. The final
issueAdd function becomes:

function issueAdd(_, { issue }) {


validateIssue(_, { issue });
[Link] = new Date();
[Link] = [Link] + 1;
[Link](issue);
return issue;
}
Next, we improve our date handling. Previously, in the GraphQLDate scalar
type, we simply returned new Date(value), but if the input date was invalid,
JavaScript wouldn't throw an error — it would just return an invalid date object.
To catch this, we check if the date is valid using isNaN(date). We do this in
both parseValue() and parseLiteral() methods of the custom scalar. Here's how
these functions look now:

parseValue(value) {
const dateValue = new Date(value);
return isNaN(dateValue) ? undefined : dateValue;
},
parseLiteral(ast) {
if ([Link] == [Link]) {
const value = new Date([Link]);
return isNaN(value) ? undefined : value;
}
}
Returning undefined causes GraphQL to treat the input as invalid and return an
error.

To help developers during debugging and also to log errors during development,
Apollo Server allows customizing how errors are formatted. We use the
formatError option to log the error to the console before sending it back. Here’s
how we add this configuration:

formatError: error => {


[Link](error);
return error;
}
We now update the Apollo Server initialization to include this setting:

const server = new ApolloServer({


typeDefs: [Link]('./server/[Link]', 'utf-8'),
resolvers,
formatError: error => {
[Link](error);
return error;
},
});
With everything implemented, you can now test the validations using the
GraphQL Playground. Remember that since status is now an enum, you should
provide it without quotes. Here’s a valid query:

mutation {
issueAdd(issue: {
title: "Completion date should be optional",
status: New
}) {
id
status
}
}
This should return a successful response like:
{
"data": {
"issueAdd": {
"id": 5,
"status": "New"
}
}
}
If you try an invalid enum value like Unknown, you will get an error saying:

{
"errors": [
{
"message": "Expected type StatusType, found Unknown."
}
]
}
And if you use a string instead of a literal enum, such as "New" (with quotes),
you’ll see a helpful message like:

{
"errors": [
{
"message": "Expected type StatusType, found \"New\"; Did you mean the
enum value New?"
}
]
}
If you skip the status field, it will automatically default to New.

To test programmatic validations, you can use a query that breaks both rules.
For example:

mutation {
issueAdd(issue: {
title: "Co",
status: Assigned
}) {
id
status
}
}
This will return both validation messages in the error:

{
"errors": [
{
"message": "Invalid input(s)",
"extensions": {
"code": "BAD_USER_INPUT",
"exception": {
"errors": [
"Field \"title\" must be at least 3 characters long.",
"Field \"owner\" is required when status is \"Assigned\""
]
}
}
}
]
}
Finally, to test invalid date inputs, try passing an incorrect date string:

mutation {
issueAdd(issue: {
title: "Completion date should be optional",
due: "not-a-date"
}) {
id
}
}
You will get an error like:

{
"errors": [
{
"message": "Expected type GraphQLDate, found \"not-a-date\"."
}
]
}
You can also test this using query variables:

mutation issueAddOperation($issue: IssueInputs!) {


issueAdd(issue: $issue) {
id
status
due
}
}
With variables:

{
"issue": {
"title": "test",
"due": "not-a-date"
}
}
This will result in an error indicating that the date could not be parsed.

This completes the full walkthrough of input validations in your Issue Tracker
app. You now have validation at both the schema level (with enums and
defaults) and at the server logic level (with custom checks and error reporting).

Displaying Errors:

Now that we have created APIs and added validations in our Issue Tracker
application, the next important step is to display error messages to the user.
These errors might happen for different reasons — for example, if the network
connection fails (known as a transport error), or if the user enters incorrect input
(like a short title or invalid status). There can also be other unexpected server
errors, although these are usually caused by bugs and are less likely to happen
during normal usage.

To handle all these types of errors in a clean and centralized way, we will create
a common utility function that performs all GraphQL API calls and handles
errors. This function will replace the existing fetch() calls in the loadData() and
createIssue() methods. The new function will also show alerts to the user when
an error occurs. We will call this function graphQLFetch.
We define graphQLFetch() as an async function that takes two arguments — the
GraphQL query, and an optional variables object that defaults to an empty
object if not provided. This default value is added using the ES2015 default
parameter syntax. The function uses await fetch() inside a try-catch block to
detect transport errors.

Here is the complete code for the function as shown in Listing 5-18:

async function graphQLFetch(query, variables = {}) {


try {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link]({ query, variables }),
});
const body = await [Link]();
const result = [Link](body, jsonDateReviver);
if ([Link]) {
const error = [Link][0];
if ([Link] == 'BAD_USER_INPUT') {
const details = [Link]('\n ');
alert(`${[Link]}:\n ${details}`);
} else {
alert(`${[Link]}: ${[Link]}`);
}
}
return [Link];
} catch (e) {
alert(`Error in sending data to server: ${[Link]}`);
}
}
In this function, if [Link] exists in the server’s response, we check the
error code using [Link]. If it is 'BAD_USER_INPUT', we know
that validation failed on the server. We get the list of validation errors from
[Link] and join them into a single string using join('\n
'). Then we show it to the user using alert(). For any other type of error (like
internal server errors), we just show the error code and the message directly.
If an exception occurs — such as a network failure — it is caught in the catch
block, and we alert the user that data could not be sent to the server.
Now that graphQLFetch() is ready, we will replace the old fetch code in
loadData() and createIssue() methods of the IssueList component.

First, let’s update loadData(). This method sends a GraphQL query to fetch all
issues and sets them in the component’s state. Instead of manually using fetch()
and parsing the response, we now use graphQLFetch():
async loadData() {
const query = `query {
issueList {
id title status owner
effort created due
}
}`;
const data = await graphQLFetch(query);
if (data) {
[Link]({ issues: [Link] });
}
}
In the updated version, we call graphQLFetch() and store the result in data. If
data is not null, we update the state with the issues returned from the server.
Next, we update the createIssue() method to also use graphQLFetch(). Here, we
are sending a GraphQL mutation using a variable $issue, so we need to pass the
query and the variables object. After the mutation succeeds, we call loadData()
again to refresh the list:
async createIssue(issue) {
const query = `mutation issueAdd($issue: IssueInputs!) {
issueAdd(issue: $issue) {
id
}
}`;
const data = await graphQLFetch(query, { issue });
if (data) {
[Link]();
}
}
Just like before, if the server returns data, we reload the list of issues using
[Link]().
Now let’s test all of this. To simulate a transport error, you can stop the backend
server while the browser is open and then try to add a new issue. Since the
fetch() call fails, the catch block will be triggered, and you will see an alert like
this:
Error in sending data to server: Failed to fetch
This is shown in the browser as a popup and is useful for diagnosing network
issues.

To test user input validations, you can try typing a title that is too short — for
example, just two characters — and click Add. The server will return a
validation error, and graphQLFetch() will display a message like this:
Invalid input(s):
Field "title" must be at least 3 characters long.
Other validations, like checking if owner is required when status is Assigned, or
if a date is invalid, can be tested by temporarily editing the IssueAdd
component’s handleSubmit() method to add invalid values. For example, you
can force the status to Assigned but leave the owner field empty, or set due to
"not-a-date".
This finishes the error handling part of the application. We now have a reusable
utility (graphQLFetch) that all API calls go through. It handles:
 Fetching data from the server
 Detecting and showing transport errors
 Parsing server errors
 Displaying helpful alerts for validation failures
 Printing other unexpected server messages

The app is now much more robust and user-friendly, especially when something
goes wrong.

You might also like