Understanding The List API in GraphQL With Examples
Understanding The List API in GraphQL With Examples
Examples
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" },
];
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;
},
},
};
// Start server
[Link]().then(({ url }) => {
[Link](`🚀 Server ready at ${url}`);
});
// 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.
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.
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
}
}`;
[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:
{
"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:
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:
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
}
}`;
[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.
Now we modify the issueAdd resolver to include this validation call. The final
issueAdd function becomes:
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:
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:
{
"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:
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.