TL;DR
In this guide, we’ll be showing you how to use GraphQL alongside React and GraphQL Codegen to create a simple page that can pull data from an API and send emails. We’ll be using Novu as an open source notification system for developers that can send our emails after being passed through a form created within React.
Intro
We’ve all been there: you’ve been coding away in React with Typescript enjoying type-checking until it’s time to time to integrate your application with an API. And then, all of your data types are wrong, ending up with a ton of errors, or just not working at all. Thankfully, integrating APIs doesn’t need to be complicated with GraphQL. It enables you to quickly get an API integrated without the hassle of a standard REST API, thanks to tools like Codegen.
In this article, I will demonstrate the problem that many developers face and how to improve the workflow when integrating your react app with a GraphQL API.
This tutorial assumes that you have been working with React before and want to improve, so you already have your workstation set up.
What We Will Do:
- Bootstrap a new Next.js TypeScript project
- Install Apollo GraphQL and fetch data from a public API
- Showcase why using GraphQL Codegen is a good idea
- Create a Next.js API endpoint for sending emails with data
- Use the Novu.co platform for setting up and sending emails to a given email
If you want to skip the writeup, you can go to GitHub directly and look at the code: https://github.com/ugglr/next-typescript-graphql-integration
Creating an Example Project with React and Next.js
Learning is faster when you get your hands dirty, so let’s jump straight in. We’ll bootstrap a new React project using Next.js with Typescript. Next.js is a full-stack framework for React that allows us to quickly start up a web app.
Run the following command in your terminal to scaffold a new Next.js project with Typescript:
yarn create next-app --typescript
It will then prompt you for the name of the project. Let’s name it next-typescript-graphql-integration
so we know what we’re working with, and we’ll leave anything else to default values.
While we’re still in the terminal, navigate to our newly created project and run it to make sure that the installation was successful.
cd next-typescript-graphql-integration
yarn dev
The output will be similar to this:
yarn dev
yarn run v1.22.15
$ next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 1457 ms (165 modules)
From this, we can determine that the page is available at localhost:3000. Opening the browser and checking the page will show the default Next.js screen.
Install Dependencies & Initialize Apollo Client
We are now ready to install some packages. We start by installing one of the most popular GraphQL clients, Apollo Client. Apollo is a tool that enables devs to utilize GraphQL APIs within almost any tech stack and integrates it within your UI. Then, we’ll start doing some fetching from this public Rick & Morty GraphQL API.
yarn add @apollo/client graphql
When that’s done, let’s open up our code editor and initialize the Apollo Client. When inspecting the folder you can see that it’s a standard bare-bones Next project with the following folder structure:
root
--- .next
--- pages
--- public
--- styles
Inside of _app.tsc
we can start initializing our client by following the steps described by the official Apollo Client React docs.
We include the necessary imports and create the client, then include it in our React app via ApolloProvider
. In the end, the file should look something like this:
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
const client = new ApolloClient({
uri: "https://rickandmortyapi.com/graphql",
cache: new InMemoryCache(),
});
export default function App({ Component, pageProps }: AppProps) {
return (
<ApolloProvider {...{ client }}>
<Component {...pageProps} />
</ApolloProvider>
);
}
This is everything we need to initialize our brand new GraphQL client on the client side. 🚀 Let’s continue with fetching some data the un-safe way.
Fetching Data without Type Checking
Now that we have our project up and running, it’s time to add some data from our API. If we want to get a list of characters from the API we need to run a query like so:
query {
characters {
results {
id
name
species
}
}
}
The Apollo documentation will tell us to create the query document and then use it together with the useQuery
hook provided by Apollo. I’ve deleted everything in index.tsx
and replaced it with the following:
import { gql, useQuery } from "@apollo/client";
import { NextPage } from "next";
const GET_CHARACTERS = gql`
query {
characters {
results {
id
name
species
}
}
}
`;
const Home: NextPage = () => {
const { loading, data } = useQuery(GET_CHARACTERS);
return (
<div>
<main>
{loading && <p>Loading...</p>}
{data?.characters.results.map((character) => (
<p key={character.id}>{character.name}</p>
))}
</main>
</div>
);
};
export default Home;
This is what it will look like, so let’s check back to our application:
Everything seems like it’s good, right?
What Went Wrong with GraphQL?
While our code may work as intended, it’s not exactly reliable. There are two common errors we made here that we need to look at:
No GraphQL Type Validation
We didn’t add any type validation to our GraphQL calls. This means if you request a field that doesn’t exist in the schema, you will get weird and hard-to-understand error messages. Let’s see what can happen if we change our project:
const GET_CHARACTERS = gql`
query {
characters {
results {
id
name
species
notInSchema // 👈 this will result in error
}
}
}
`;
Running this will give us an “Error! Response not successful: Received status code 400”, which doesn’t tell you what went wrong at all, becoming a huge time waster.
Considering that some queries and mutations might have a variety of variables, fields, and subfields this gets complex fast (believe me, I’ve done this to my breaking point before!). So you should be ensuring that your GraphQL queries don’t call for items not in the schema.
No Types In The Response
We also didn’t include any form of autocomplete in our response, so our code doesn’t know what is supposed to be returned from our API. We need to ensure that we validate the data we receive from our API to ensure our data is correct. If we don’t, this can become a nightmare in the future.
Let’s take a look at our code editor. You should see a red line under the character
variable with the following Typescript validation error:
So how do we fix this issue?
If we’re just building something small, we can create our own type like so:
type GetCharactersQueryResponse = {
characters: {
results: Array<{
id: string;
name: string;
species: string;
}>;
};
};
And then passing it into our query hook:
const { loading, error, data } =
useQuery<GetCharactersQueryResponse>(GET_CHARACTERS);
This will give us type-checking from Typescript and work just fine.
But this solution becomes a nightmare when return types become large or if you need to pass variables. All of those are checked in the GraphQL API and will send you confusing errors that are hard to debug.
Fortunately, there are some smart people who realized that all this information is already available inside GraphQL schemas & can be used to generate this information for us.
Enter: GraphQL Codegen 🚀
What Is Graphql Codegen?
Graphql Codegen is a code generation library for GraphQL that enables developers to generate custom code. It provides us developers with the ability to generate type definitions, query builders, documentation, and more by analyzing our GraphQL schemas. This makes it easier and faster to build GraphQL applications and reduces the time spent coding.
Additionally, Graphql Codegen is also a type-safe code generator. This means that the generated code is checked for errors by the Graphql Codegen compiler before it is used in an application. This ensures that any errors in the code are caught before they cause problems in production.
Plus, it also provides developers with an easy way to manage GraphQL schemas. It allows devs to easily add, remove, and update their schemas without any hassle. This makes it much easier to keep your schemas up-to-date.
Setup and Configuring GraphQL Codegen
Before we can start using Graphql Codegen, we’ll need to configure it. This process is relatively simple and can be done in a few steps:
The first step is installing GraphQL Codegen by running this command 👨💻
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-graphql-request @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
This will install the following packages as devDepencencies
@graphql-codegen/cli
@graphql-codegen/typescript
@graphql-codegen/typescript-graphql-request
@graphql-codegen/typescript-operations
@graphql-codegen/typescript-react-apollo
After installing, we need to configure GraphQL Codegen. I prefer to do this by adding a .yml
file to the root of our directory.
$ touch graphql.config.yml
Open up the file and let’s add the following configuration:
schema:
- "https://rickandmortyapi.com/graphql"
documents:
- "./graphql/**/*.graphql"
generates:
./generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
The configuration file does the following:
-
schema
This points to our GraphQL endpoint for fetching the API schema map. -
documents
This tells GraphQL Codegen where to look for our schema files -
generates
This tells GraphQL Codegen where to create and store generated code. -
plugins
Specifies what GraphQL Codegen plugins to use.
To finalize the setup we also need to add the generation script to our package.json
{
"name": "next-typescript-graphql-integration",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"generate": "graphql-codegen --config graphql.config.yml" 👈 here
},
"dependencies": {
"@apollo/client": "^3.7.7",
"@next/font": "13.1.6",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"eslint": "8.33.0",
"eslint-config-next": "13.1.6",
"graphql": "^16.6.0",
"next": "13.1.6",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
},
"devDependencies": {
"@graphql-codegen/cli": "^3.0.0",
"@graphql-codegen/typescript": "^3.0.0",
"@graphql-codegen/typescript-graphql-request": "^4.5.8",
"@graphql-codegen/typescript-operations": "^3.0.0",
"@graphql-codegen/typescript-react-apollo": "^3.3.7"
}
}
If you run yarn generate
now you will see an error output like this:
yarn generate
yarn run v1.22.15
$ graphql-codegen --config ./graphql.config.yml
(node:15632) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Parse Configuration
❯ Generate outputs
✔ Parse Configuration
⚠ Generate outputs
❯ Generate to ./generated/graphql.ts
✔ Load GraphQL schemas
✖
Unable to find any GraphQL type definitions for the f…
- ./graphql/**/*.graphql
◼ Generate
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
This means the setup is complete and we are ready to add some schema files.
Adding Our Schema File
Starting off, create a folder in the root of the directory called graphql
, and inside create a file called get-characters.query.graphql
. We already told Codegen where to look for our schemas, but the second filename is up to you.
The folder structure now looks like this 👇
root
--- .next
--- pages
--- public
--- styles
--- graphql 👈 here 🤩
Then inside of the file get-characters.query.graphql
, add the query that we previously used, but without wrapping it in a gql
template string, like this:
query GetCharacters {
characters {
results {
id
name
species
}
}
}
And then re-run the generate
script, and you will see the following:
yarn generate
yarn run v1.22.15
$ graphql-codegen --config ./graphql.config.yml
(node:16009) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Parse Configuration
❯ Generate outputs
✔ Parse Configuration
✔ Generate outputs
✨ Done in 7.43s.
Congratulations! We are almost done. Now, check root
and you will notice that the script has created a new folder called generated
with a file called graphql.ts
. This is as we configured in the graphql.config.file
.
root
--- .next
--- pages
--- public
--- styles
--- graphql
--- generated 👈 here 👀
If you inspect that file you can see that we have generated a bunch of types pulled from the API and generated custom React hooks for the query that we authored in get-characters.query.graphql
.
GraphQL Codegen in Action
Now that you have Graphql Codegen set up and configured, you can start using it in your React application. Let’s modify index.tsx
to the following:
import {
GetCharactersDocument,
GetCharactersQuery,
GetCharactersQueryVariables,
} from "@/generated/graphql";
import { useQuery } from "@apollo/client";
import { NextPage } from "next";
const Home: NextPage = () => {
const { loading, data } = useQuery<
GetCharactersQuery,
GetCharactersQueryVariables
>(GetCharactersDocument);
const characters = data?.characters?.results;
return (
<div>
<main>
{loading && <p>Loading...</p>}
{characters &&
characters.map((character, index) => (
<p key={character?.id ?? index}>
{character?.name ?? "No name: something is wrong"}
</p>
))}
</main>
</div>
);
};
export default Home;
And now, our data is type-safe and validated. If there was something wrong within the query that didn’t match the API, it would have thrown an error during generation. Now, inside our component, we now have fully typed data, which can be demonstrated by hovering over character
.
But where are the custom hooks?
Let’s take it one step further and use the typescript-react-apollo
plugin we told GraphQL Codegen to use. This will make our code super clean.
Using Generated End-To-End Type-Safe Apollo Hooks
Let’s clean up our code a little bit to something like this:
import { NextPage } from "next";
import { useGetCharactersQuery } from "@/generated/graphql";
const Home: NextPage = () => {
// This hook is validated towards the API & will infer
// types and throw you type errors if you have not
// provided the right variables.
const { loading, data } = useGetCharactersQuery();
const characters = data?.characters?.results;
return (
<div>
<main>
{loading && <p>Loading...</p>}
{characters &&
characters.map((character, index) => (
<p key={character?.id ?? index}>
{character?.name ?? "No name: something is wrong"}
</p>
))}
</main>
</div>
);
};
export default Home;
Look at how neat and clean that is! 🤩 And it’s still typed, too!
For this project to continue API integration, all developers need to do is add new files into the graphql
folder and run yarn generate
before restarting the dev server. Then, everything gets generated automatically.
One more check to see if it works. Don’t forget to start your development server if you stopped it.
Obviously, this is a very small app so far. Even so, I think I have been able to demonstrate that even at this scale, GraphQL Codegen is a very valuable tool.
Best Practices for GraphQL Codegen
There are some best practices that developers should follow when using GraphQL Codegen:
First, it is important to keep your GraphQL Codegen configuration file up-to-date. This will ensure that the generated code is always accurate. It’s also important to make sure your configuration file is well-structured and easy to understand. This makes it easier for changes in the future.
Second, for production projects, it is important to test your generated code. This ensures the code is working as expected and any errors are caught before they cause problems in production.
Adding In New Functionality With Novu
Showing data is pretty neat, but what if we could aggregate that data and send it to our users?
For this, we can use Novu!
Novu is an open-source notification infrastructure for developers. They make it possible to send notifications though a unified API. With their platform, it’s possible to bundle notification sending into “triggers”.
Managing all notification handlers normally becomes a big chore for us developers as our applications become more complex. Only handling code for email notifications might be doable in a small team, but adding in SMS and push notifications quickly becomes a time drain to get up and running. Which is exactly what Novu intends to help with.
How to Get Started with Novu
The quickest way to get started with Novu is by signing up for their cloud platform. Its totally free, and with a generous free tier of 10k events per month, you’ll have plenty to work with. There is also the option to self-host the platform on your own servers but will require extra work. For this tutorial, we’ll be going with the simpler option through Novu’s cloud platform.
Step 1: Sign Up
After signing up they will ask for an organization name, and then you will be greeted with a welcome screen.
Step 2: Connect the Novu Email Provider
For most providers, Novu sits between them and your application. That means if you are sending an SMS to your user, Novu doesn’t send that, it’s done by your provider.
However, to get started, I recommend using their built-in email provider. It lets you send up to 300 emails, which is plenty for small applications like ours. The emails will come from no-reply@novu.co, and the sending will be the organization name you picked earlier.
Step 3: Create a New Notification Trigger
We also need to define a trigger by going to Notifications in the cloud panel.
Press New and add our trigger.
This is what our settings should look like, but feel free to switch it up if you want:
Then head into the Workflow Editor and configure what happens when triggered.
Add an email field and then enter the email editor to edit the template or any other settings you’d like to add.
Email templates often inject variables with {{ VARIABLE }}
. This is how we’ll inject the request payload into our email template. Apply variables in the editor, and you’ll see them show up in the variables box. In this case, {{ name }}
and {{ species }}
will need to be present in the payload we send to the API. This is what our template should look like:
Press update and see that the payload variables show up in the variables box:
That’s all we need for now! Let’s continue with writing the components and wire everything together. 🪡
Step 4: Create the React Form Component
Let's jump back into the code and create a form to take our users’ email and store it locally.
We will create a folder in root
called components
, with a file called EmailForm.tsx
.
import { FormEvent, useState } from "react";
const EmailForm = () => {
const [success, setSuccess] = useState<boolean>(false);
const [email, setEmail] = useState<string>("");
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// send the email to user
};
return (
<div>
<p>Send me random character 🚀</p>
<form onSubmit={onSubmit}>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Send!</button>
</form>
</div>
);
};
export default EmailForm;
With our form set up and ready to go, we can connect it to the Novu API!
Step 5: Install Novu-client
The easiest way to interact with the Novu API is to install their client package. We can install the @novu/node
package from npm by running yarn add @novu/node
.
After this, create a folder in root
called lib
which will hold the logic for interacting with the Novu API. We’ll call it novu.ts
, and it’s going to export an async function which we can then re-use.
Then, we’ll head into the Novu Dashboard and grab this Node.js code snippet:
import { Novu } from '@novu/node';
const novu = new Novu('<API_KEY>');
novu.trigger('randm-random-email', {
to: {
subscriberId: '<REPLACE_WITH_DATA>',
email: '<REPLACE_WITH_DATA>'
},
payload: {
name: '<REPLACE_WITH_DATA>',
species: '<REPLACE_WITH_DATA>'
}
});
Pretty neat! They’ve done most of the work for us ☕️. Novu adds the payload variables and everything, saving us time from going to the docs.
Now back to the editor:
// lib/novu.ts
import { Novu } from "@novu/node";
const novu = new Novu("<API_KEY>");
type Payload = {
name: string;
species: string;
};
export const sendEmail = async (email: string, payload: Payload) => {
if (!email) throw new Error("No email");
await novu.trigger("<REPLACE_WITH_TRIGGER_ID>", {
to: {
email,
subscriberId: email,
},
payload,
});
};
This is how the payload should look in the new Payload
type, which will be sent to Novu and injected into our email template. Replace API_KEY
and TRIGGER_ID
with our variables, and we should be good to go!
Note: Before pushing this to git, please make sure you’re using environmental variables, not what’s shown in this tutorial.
Step 6: Create a Next.js API Endpoint
Next.js is a full-stack framework for React, and they make it easy for us to create full-stack apps. One of the most notable features is that we can create API endpoints on our server. These are then hidden from the client browser for extra security.
The way these works is by adding a new file to our pages
folder and using the folder structure to define our endpoints. The below structure will create an endpoint at /send-email
:
root
--- .next
--- pages
--- api
--- hello.ts
--- send-email.ts
--- public
--- styles
--- graphql
--- generated
And here’s the code for the handler, where we get our parameters from the request:
import { sendEmail } from "@/lib/novu";
import { NextApiRequest, NextApiResponse } from "next";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// we are only handling post requests.
if (req.method === "POST") {
try {
const { email, name, species } = JSON.parse(req.body);
if (!email || !name || !species) {
// return bad request status.
res.status(400).end();
return;
}
await sendEmail(email, { name, species });
res.status(200).end();
} catch (error) {
// Just response internal server error;
res.status(500).end();
}
}
};
export default handler;
This handler will:
- Look at any incoming
POST
requests to the endpoint - Check if the body of the request has the necessary params
- Send the email if everything is correct, or return an error if something is wrong
And that’s all we need for our backend! So let’s add in the client-side.
Step 7: Add Our Email Functionality
Now that we’re back at the client-side, let’s add in the rest of the functionality. We’ll start at the top of the tree in index.tsx
and import our EmailForm.tsx
component like so:
import { NextPage } from "next";
import { useGetCharactersQuery } from "@/generated/graphql";
import EmailForm from "@/components/EmailForm";
const Home: NextPage = () => {
const { loading, data } = useGetCharactersQuery();
const characters = data?.characters?.results;
return (
<div>
<main>
{loading && <p>Loading...</p>}
{characters && (
<div>
<EmailForm {...{ characters }} />
{characters.map((character, index) => (
<p key={character?.id ?? index}>
{character?.name ?? "No name: something is wrong"}
</p>
))}
</div>
)}
</main>
</div>
);
};
export default Home;
That was simple! Let’s finish the implementation in EmailForm.tsx
:
import { Character } from "@/generated/graphql";
import { FormEvent, useState } from "react";
type Props = {
characters: (Character | null)[] | null | undefined;
};
const EmailForm: React.FC<Props> = ({ characters }) => {
const [email, setEmail] = useState<string>("");
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (characters && characters?.length > 0) {
// Get random character from the array.
const random = Math.floor(Math.random() * (characters.length - 1));
const randomCharacter = characters[random];
// Make send email request to /api/send-email
fetch("http://localhost:3000/api/send-email", {
method: "POST",
body: JSON.stringify({
email,
name: randomCharacter?.name,
species: randomCharacter?.species,
}),
});
}
};
return (
<div>
<p>Send me random character 🚀</p>
<form onSubmit={onSubmit}>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Send!</button>
</form>
</div>
);
};
export default EmailForm;
This form component will:
- Render a text input for email, and store the data in local storage
- Submit the data when the button has been pressed
- Makes a request to our API which sends a request to Novu.co, which in turn will send en email to the given email address
And that’s it! Now we need to test it out.
Step 8: Send Emails!
If we take a look in the browser at this point, it should look like this:
It’s not pretty, but it’s functional!
Enter your email and see if it shows up in your inbox!
Wrapping It Up
Integrating APIs into a project can be difficult, but it doesn’t have to be. And with GraphQL, it’s easier than ever before! It allows you to set up API integrations with ease and can pair with tools like GraphQL Codegen to automate some of the hassles.
Paired with a powerful notification platform like Novu, you can easily get a project up and running in no time, with notifications and communication options at your disposal.
And the best part? You can do it all for free! No buy-in required. I know I’ll be using Novu in my future, what about you?
Full source code can be found here https://github.com/ugglr/next-typescript-graphql-integration