How to Use GraphQL API in Node.js Apps
By: John Au-Yeung
Tags: JavaScript, GraphQL, Technology, Software Development, Software

GraphQL provides lots of benefits that aren’t available in traditional REST APIs. GraphQL provides strong typing for inputs and outputs, so it’s much harder to enter invalid inputs and unexpected output. You have to specify what fields you want in your output so you won’t be getting a lot of useless data that you don’t need.

Also, GraphQL requests are made over regular HTTP, so normal HTTP clients can make GraphQL requests.

You make requests like this:

{

getPhotos(page: 1) {

photos {

id

fileLocation

description

tags

}

page

totalPhotos

}

}

The top line has the input arguments, and the rest are the fields that you want to get.

To make making requests easier, there are GraphQL clients available. For Node.js, the graphql-requests package is a great library for making GraphQL requests.

One example of a GraphQL API is the Yelp GraphQL API. It’s great getting information about businesses and events all over the world. It is available for free and the rate limit is high. It’s perfect for using it to build an app.

It does not support cross-domain requests, so you have to use the back end to access the data if you want to feed it to the front end.

You have to register for an API key to use the Yelp API. Register for one at https://www.yelp.com/developers/documentation/v3/authentication if you want to use it. As with any API key, it should be kept at a secret location.

A lot of data about businesses is available. The API returns name, phone, price level, categories, hours, reviews, photos, etc. It has pretty much anything most people want to know about a business.

In this article, we will build an app that uses the Yelp API to search for data. We need a back end app to access the Yelp API since it cannot be accessed directly from the browser. We will use the GraphQL API to search for business and events data. It will be built with Express. The front end app will be a React app that provides search pages for users to search the Yelp API for business and events via the back end app.

To start we make an empty project folder. Then we make a backend folder in the project folder and in it, run the Express Generator to generate the skeleton code by running npx express-generator .

Once that’s done, we install some packages that we need. We need Babel to use the latest JavaScript features, the CORS package to enable cross-domain requests in our Express app, dotenv to store environment variables like the Yelp API key, and graphql-request package to make GraphQL requests to the Yelp API.

To do this, run npm i @babel/cli @babel/core @babel/node @babel/preset-env cors graphql-request . Once that’s done add the following to the scripts section of package.json :

"start": "nodemon --exec npm run babel-node -- ./bin/www",

"babel-node": "babel-node"

We then need to create a file called .babelrc in the backend folder and add:

to specify that the project will use the latest JavaScript features.

This will let users run our app using Babel Node instead of regular Node.js, which supports the latest JavaScript features. We will also need to install nodemon to watch for changes in our files and reload when we develop the app.

Next, we can write some logic. We need routes to get the data from the front end to the Yelp API, then return the results from the Yelp API as JSON. Create a file called yelp.js and add the following code:



const router = express.Router();

const yelpApiUrl = "

import { GraphQLClient } from "graphql-request";

const client = new GraphQLClient(yelpApiUrl, {

headers: { Authorization: `Bearer ${process.env.YELP_API_KEY}` },

}); const express = require("express");const router = express.Router();const yelpApiUrl = " https://api.yelp.com/v3/graphql ";import { GraphQLClient } from "graphql-request";const client = new GraphQLClient(yelpApiUrl, {headers: { Authorization: `Bearer ${process.env.YELP_API_KEY}` },}); /* GET users listing. */

router.post("/business/search", async (req, res, next) => {

const query = `

query search($term: String!, $location: String!, $offset: Int!) {

search(

term: $term,

location: $location,

offset: $offset

) {

total

business {

name

rating

review_count

hours {

is_open_now

open {

start

end

day

}

}

location {

address1

city

state

country

}

}

}

}`; const data = await client.request(query, req.body);

res.json(data);

}); router.post("/events/search", async (req, res, next) => {

const query = `

query event_search($location: String!, $offset: Int!) {

event_search(

location: $location,

offset: $offset

) {

total

events {

name

cost

cost_max

}

}

}`; const data = await client.request(query, req.body);

res.json(data);

}); router.post("/phone/search", async (req, res, next) => {

const query = `

query phone_search($phone: String!) {

phone_search(

phone: $phone

) {

total

business {

name

rating

review_count

location {

address1

city

state

country

}

hours {

is_open_now

open {

start

end

day

}

}

}

}

}`; const data = await client.request(query, req.body);

res.json(data);

}); module.exports = router;

In each route, we have a query for accessing data from the Yelp GraphQL API. we created the GraphQLClient by instantiating with the YELP_API_KEY , which we stored in the .env file in the backend folder.

The .env file should be like this:

YELP_API_KEY='your api key'

To get the queries for the API, we use the GraphQL API sandbox located at https://www.yelp.com/developers/graphql/query/business to fiddle with the query until we get the one that we want. The sandbox has autocompleted so you don’t have to guess what is available. This is the beauty of GraphQL APIs. The inputs and outputs are strongly typed so you know what you can input and what you can get as output. There is also the API documentation above the sandbox.

The first line of each query string, for example, query search($term: String!, $location: String!, $offset: Int!) , specifies the input of the query. The exclamation mark means it’s required. With these parameters specified, we can pass in variables as objects in the second argument of the client object to pass in the fields as variables.

In the end, we get the JSON response like with any other GraphQL API and send the result back to the client.

Next, we replace the existing code in app.js with the following:

require('dotenv').config();

const createError = require("http-errors");

const express = require("express");

const path = require("path");

const cookieParser = require("cookie-parser");

const logger = require("morgan");

const cors = require("cors"); const indexRouter = require("./routes/index");

const yelpRouter = require("./routes/yelp"); const app = express(); // view engine setup

app.set("views", path.join(__dirname, "views"));

app.set("view engine", "jade"); app.use(logger("dev"));

app.use(express.json());

app.use(express.urlencoded({ extended: false }));

app.use(cookieParser());

app.use(express.static(path.join(__dirname, "public")));

app.use(cors()); app.use("/", indexRouter);

app.use("/yelp", yelpRouter); // catch 404 and forward to error handler

app.use(function(req, res, next) {

next(createError(404));

}); // error handler

app.use(function(err, req, res, next) {

// set locals, only providing error in development

res.locals.message = err.message;

res.locals.error = req.app.get("env") === "development" ? err : {}; // render the error page

res.status(err.status || 500);

res.render("error");

}); module.exports = app;

In this file, we added app.use(cors()); to enable CORS in our back end app and added:

const yelpRouter = require("./routes/yelp");

app.use("/yelp", yelpRouter);

to expose the routes that we specified. And we added:

require('dotenv').config();

to load our environment variables so we can use it in our app.

Now that the back end is done, we can move on to the front end.

We start by running Create React App to create the files for the front end app. We do this by running npx create-react-app frontend in the project’s root folder.

Next, we install some packages. We need Axios for making HTTP requests to our back end app, Bootstrap for styling, React Router for routing, and Formik and Yup for handling input value changes and form validation respectively.

We run npm i axios bootstrap formik react-bootstrap react-router-dom yup to install all the packages.

Now we are ready to write some code. Everything will be in the src folder unless otherwise specified. In App.js , we replace the existing code with the following:

import React from "react";

import "./App.css";

import TopBar from "./TopBar";

import { Router, Route, Link } from "react-router-dom";

import { createBrowserHistory as createHistory } from "history";

import HomePage from "./HomePage";

import PhoneSearchPage from "./PhoneSearchPage";

import EventSearchPage from "./EventSearchPage";

const history = createHistory(); function App() {

return (

<div className="App">

<Router history={history}>

<TopBar />

<Route path="/" exact component={HomePage} />

<Route path="/phonesearch" exact component={PhoneSearchPage} />

<Route path="/eventsearch" exact component={EventSearchPage} />

</Router>

</div>

);

} export default App;

We have routes for 3 pages. One for the home page, one for phone search, and one for event search. We also have a top bar at the top with links to these pages.

In App.css , we replace the existing code with the following:

.center {

text-align: center;

}

to center text.

Next, we create an event search page. Create a file called EventSearchPage.js and add the following:

import React, { useState } from "react";

import { Formik } from "formik";

import Form from "react-bootstrap/Form";

import Col from "react-bootstrap/Col";

import Button from "react-bootstrap/Button";

import Card from "react-bootstrap/Card";

import Pagination from "react-bootstrap/Pagination";

import * as yup from "yup";

import { searchEvents } from "./requests"; const schema = yup.object({

location: yup.string().required("Location is required"),

}); function EventSearchPage() {

const [results, setResults] = useState([]);

const [offset, setOffset] = useState(0);

const [page, setPage] = useState(1);

const [total, setTotal] = useState(0);

const [params, setParams] = useState({}); const getEvents = async params => {

const response = await searchEvents(params);

setTotal(response.data.event_search.total);

setResults(response.data.event_search.events);

}; const changePage = async page => {

setPage(page);

setOffset((page - 1) * 20);

params.offset = (page - 1) * 20;

setParams(params);

getEvents(params);

}; const handleSubmit = async evt => {

const isValid = await schema.validate(evt);

if (!isValid) {

return;

}

evt.offset = offset;

setParams(evt);

getEvents(evt);

}; return (

<div className="home-page">

<h1 className="center">Event Search</h1>

<Formik validationSchema={schema} onSubmit={handleSubmit}>

{({

handleSubmit,

handleChange,

handleBlur,

values,

touched,

isInvalid,

errors,

}) => (

<Form noValidate onSubmit={handleSubmit}>

<Form.Row>

<Form.Group as={Col} md="12" controlId="name">

<Form.Label>Location</Form.Label>

<Form.Control

type="text"

name="location"

placeholder="Location"

value={values.location || ""}

onChange={handleChange}

isInvalid={touched.location && errors.location}

/>

<Form.Control.Feedback type="invalid">

{errors.location}

</Form.Control.Feedback>

</Form.Group>

</Form.Row>

<Button type="submit">Search</Button>

</Form>

)}

</Formik>

<br />

{results.map((r, i) => {

return (

<Card key={i}>

<Card.Title className="card-title">{r.name}</Card.Title>

<Card.Body></Card.Body>

</Card>

);

})}

<br />

<Pagination>

<Pagination.First onClick={changePage.bind(this, 1)} />

<Pagination.Prev

onClick={changePage.bind(this, page - 1)}

disabled={page == 0}

/>

<Pagination.Next

onClick={changePage.bind(this, page + 1)}

disabled={Math.ceil(total / 20) == page}

/>

<Pagination.Last

onClick={changePage.bind(this, Math.max(Math.floor(total / 20)), 0)}

/>

</Pagination>

</div>

);

} export default EventSearchPage;

We wrap the React Boostrap form with the Formik component to use Formik’s form value handling features, automatically binding the input values to the evt object in the handleSubmit function. The schema object is created by using Yup to check for required fields, which is location in this form.

In the handleSubmit function, we get the entered value in the evt object in the parameter, we validate it with Yup’s schema.validate function. If isValid is true , then make the request to our API by calling the getEvents function. We set the offset so that we can view the paginated results by skipping the number of entries specified by the offset . We get the response and the set the results by calling setResults so that we can display the results in the Card component below the Form . We also have buttons for Pagination so that we can skip results to get to the ones we want. We make sure that the links are disabled when they don’t make sense, like disabling the Pagination.Next button when there are no next entries and Pagination.Last when the user is on the last page.

Next, we need an array of days of the week for the days constant to be defined. We create a file called exports.js and add the following:

export const days = [

"Sunday",

"Monday",

"Tuesday",

"Wednesday",

"Thursday",

"Friday",

"Saturday",

];

Next, we create the home page. Create a file called HomePage.js and add:

import React, { useState } from "react";

import "./HomePage.css";

import { Formik } from "formik";

import Form from "react-bootstrap/Form";

import Col from "react-bootstrap/Col";

import Button from "react-bootstrap/Button";

import Card from "react-bootstrap/Card";

import Pagination from "react-bootstrap/Pagination";

import * as yup from "yup";

import { searchBusiness } from "./requests";

import { days } from "./exports"; const schema = yup.object({

term: yup.string().required("Term is required"),

location: yup.string().required("Location is required"),

}); function HomePage() {

const [results, setResults] = useState([]);

const [offset, setOffset] = useState(0);

const [page, setPage] = useState(1);

const [total, setTotal] = useState(0);

const [params, setParams] = useState({}); const getBusiness = async params => {

const response = await searchBusiness(params);

setTotal(response.data.search.total);

setResults(response.data.search.business);

}; const changePage = async page => {

setPage(page);

setOffset((page - 1) * 20);

params.offset = (page - 1) * 20;

setParams(params);

getBusiness(params);

}; const handleSubmit = async evt => {

const isValid = await schema.validate(evt);

if (!isValid) {

return;

}

evt.offset = offset;

setParams(evt);

getBusiness(evt);

}; return (

<div className="home-page">

<h1 className="center">Business Search</h1>

<Formik validationSchema={schema} onSubmit={handleSubmit}>

{({

handleSubmit,

handleChange,

handleBlur,

values,

touched,

isInvalid,

errors,

}) => (

<Form noValidate onSubmit={handleSubmit}>

<Form.Row>

<Form.Group as={Col} md="12" controlId="name">

<Form.Label>Term</Form.Label>

<Form.Control

type="text"

name="term"

placeholder="Term"

value={values.term || ""}

onChange={handleChange}

isInvalid={touched.term && errors.term}

/>

<Form.Control.Feedback type="invalid">

{errors.term}

</Form.Control.Feedback>

</Form.Group>

<Form.Group as={Col} md="12" controlId="url">

<Form.Label>Location</Form.Label>

<Form.Control

type="text"

name="location"

placeholder="Location"

value={values.location || ""}

onChange={handleChange}

isInvalid={touched.location && errors.location}

/> <Form.Control.Feedback type="invalid">

{errors.location}

</Form.Control.Feedback>

</Form.Group>

</Form.Row>

<Button type="submit">Search</Button>

</Form>

)}

</Formik>

<br />

{results.map((r, i) => {

return (

<Card key={i}>

<Card.Title className="card-title">{r.name}</Card.Title>

<Card.Body>

<p>

Address: {r.location.address1}, {r.location.city},{" "}

{r.location.country}, {r.location.state}

</p>

<p>Rating: {r.rating}</p>

<p>Open Now: {r.hours[0].is_open_now ? "Yes" : "No"}</p>

<p>Hours:</p>

<ul>

{r.hours[0] &&

r.hours[0].open.map((h, i) => (

<li key={i}>

{days[h.day]}: {h.start} - {h.end}

</li>

))}

</ul>

</Card.Body>

</Card>

);

})}

<br />

<Pagination>

<Pagination.First onClick={changePage.bind(this, 1)} />

<Pagination.Prev

onClick={changePage.bind(this, page - 1)}

disabled={page == 0}

/>

<Pagination.Next

onClick={changePage.bind(this, page + 1)}

disabled={Math.ceil(total / 20) == page}

/>

<Pagination.Last

onClick={changePage.bind(this, Math.max(Math.floor(total / 20)), 0)}

/>

</Pagination>

</div>

);

} export default HomePage;

It is very similar to the EventSearchPage except that we have more fields. We also have extra data in the Card components from the results. The logic for getting the data and pagination are the same as EvenrSearchPage except that we are getting businesses instead of events.

Next, we create a file called HomePage.css and add:

.home-page {

padding: 20px;

} .card-title {

margin: 0 20px;

} export default HomePage;

to add some margins to our text and padding to our page.

Next, we make a page for searching for businesses by phone. Create a file called PhoneSearchPage.js and add the following:

import React, { useState } from "react";

import { Formik } from "formik";

import Form from "react-bootstrap/Form";

import Col from "react-bootstrap/Col";

import Button from "react-bootstrap/Button";

import Card from "react-bootstrap/Card";

import Pagination from "react-bootstrap/Pagination";

import * as yup from "yup";

import { searchPhone } from "./requests";

import { days } from "./exports"; const schema = yup.object({

phone: yup.string().required("Phone is required"),

}); function PhoneSearchPage() {

const [results, setResults] = useState([]);

const [offset, setOffset] = useState(0);

const [page, setPage] = useState(1);

const [total, setTotal] = useState(0);

const [params, setParams] = useState({}); const getEvents = async params => {

const response = await searchPhone(params);

setTotal(response.data.phone_search.total);

setResults(response.data.phone_search.business);

}; const changePage = async page => {

setPage(page);

setOffset((page - 1) * 20);

params.offset = (page - 1) * 20;

setParams(params);

getEvents(params);

}; const handleSubmit = async evt => {

const isValid = await schema.validate(evt);

if (!isValid) {

return;

}

evt.offset = offset;

setParams(evt);

getEvents(evt);

}; return (

<div className="home-page">

<h1 className="center">Phone Search</h1>

<Formik validationSchema={schema} onSubmit={handleSubmit}>

{({

handleSubmit,

handleChange,

handleBlur,

values,

touched,

isInvalid,

errors,

}) => (

<Form noValidate onSubmit={handleSubmit}>

<Form.Row>

<Form.Group as={Col} md="12" controlId="name">

<Form.Label>Phone</Form.Label>

<Form.Control

type="text"

name="phone"

placeholder="Phone"

value={values.phone || ""}

onChange={handleChange}

isInvalid={touched.phone && errors.phone}

/>

<Form.Control.Feedback type="invalid">

{errors.phone}

</Form.Control.Feedback>

</Form.Group>

</Form.Row>

<Button type="submit">Search</Button>

</Form>

)}

</Formik>

<br />

{results.map((r, i) => {

return (

<Card key={i}>

<Card.Title className="card-title">{r.name}</Card.Title>

<Card.Body>

<p>

Address: {r.location.address1}, {r.location.city},{" "}

{r.location.country}, {r.location.state}

</p>

<p>Rating: {r.rating}</p>

<p>Open Now: {r.hours[0].is_open_now ? "Yes" : "No"}</p>

<p>Hours:</p>

<ul>

{r.hours[0] &&

r.hours[0].open.map((h, i) => (

<li key={i}>

{days[h.day]}: {h.start} - {h.end}

</li>

))}

</ul>

</Card.Body>

</Card>

);

})}

<br />

<Pagination>

<Pagination.First onClick={changePage.bind(this, 1)} />

<Pagination.Prev

onClick={changePage.bind(this, page - 1)}

disabled={page == 0}

/>

<Pagination.Next

onClick={changePage.bind(this, page + 1)}

disabled={Math.ceil(total / 20) == page}

/>

<Pagination.Last

onClick={changePage.bind(this, Math.max(Math.floor(total / 20)), 0)}

/>

</Pagination>

</div>

);

} export default PhoneSearchPage;

It is like the HomePage except that we are searching by phone instead of keyword and location. Pagination is the same as HomePage .

Next we create a code file to store the requests. Create a file called requests.js and add:



const apiUrl = " const axios = require("axios");const apiUrl = " http://localhost:3000 "; export const searchBusiness = data =>

axios.post(`${apiUrl}/yelp/business/search`, data); export const searchEvents = data =>

axios.post(`${apiUrl}/yelp/events/search`, data); export const searchPhone = data =>

axios.post(`${apiUrl}/yelp/phone/search`, data);

These are the code for making the HTTP requests that we imported to the pages.

Next, we create the top bar. Create a file called TopBar.js and add:

import React from "react";

import Navbar from "react-bootstrap/Navbar";

import Nav from "react-bootstrap/Nav";

import { withRouter } from "react-router-dom"; function TopBar({ location }) {

const { pathname } = location; return (

<Navbar bg="primary" expand="lg" variant="dark">

<Navbar.Brand href="#home">Yelp App</Navbar.Brand>

<Navbar.Toggle aria-controls="basic-navbar-nav" />

<Navbar.Collapse id="basic-navbar-nav">

<Nav className="mr-auto">

<Nav.Link href="/" active={pathname == "/"}>

Business Search

</Nav.Link>

<Nav.Link href="/eventsearch" active={pathname == "/eventsearch"}>

Events Search

</Nav.Link>

<Nav.Link href="/phonesearch" active={pathname == "/phonesearch"}>

Phone Search

</Nav.Link>

</Nav>

</Navbar.Collapse>

</Navbar>

);

} export default withRouter(TopBar);

We use the Navbar provided by React Bootstrap and add the links to navigate to the pages we created. We set the active prop by checking which page we are currently in by getting the pathname from the location prop, which is provided by the withRouter function that wrapped outside the TopBar .

Finally, in index.html , replace the existing code with:



<html lang="en">

<head>

<meta charset="utf-8" />

<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />

<meta name="viewport" content="width=device-width, initial-scale=1" />

<meta name="theme-color" content="#000000" />

<meta

name="description"

content="Web site created using create-react-app"

/>

<link rel="apple-touch-icon" href="logo192.png" />

<!--

manifest.json provides metadata used when your web app is installed on a

user's mobile device or desktop. See

-->

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

<!--

Notice the use of %PUBLIC_URL% in the tags above.

It will be replaced with the URL of the `public` folder during the build.

Only files inside the `public` folder can be referenced from the HTML.

work correctly both with client-side routing and a non-root public URL.

Learn how to configure a non-root public URL by running `npm run build`.

-->

<title>Yelp App</title>

<link

rel="stylesheet"

href="

integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"

crossorigin="anonymous"

/>

</head>

<body>

<noscript>You need to enable JavaScript to run this app.</noscript>

<div id="root"></div>

<!--

This HTML file is a template.

If you open it directly in the browser, you will see an empty page. Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" willwork correctly both with client-side routing and a non-root public URL.Learn how to configure a non-root public URL by running `npm run build`.--> Yelp App https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"crossorigin="anonymous"/> You need to enable JavaScript to run this app. You can add webfonts, meta tags, or analytics to this file.

The build step will place the bundled scripts into the <body> tag. To begin the development, run `npm start` or `yarn start`.

To create a production bundle, use `npm run build` or `yarn build`.

-->

</body>

</html>

to change the title and add the Bootstrap CSS to our app.