Front-end and Back-end Integration with React and Node.js [Full-stack Tutorial Part 3]

ProfilePicture of Pedro Manfroi
Pedro Manfroi
Senior Full-stack Developer

In this tutorial series, we’re exploring the core concepts and benefits of using GraphQL with Node.js and TypeScript architecture as we build a full-stack application together. So far, we’ve prepared our back-end to expose a GraphQL API – if you missed it, check out part one and two of the series. 

In this final chapter, we’re going to build our front-end and implement the user-facing features of our app. 

Helpful Resources for this Tutorial

In this tutorial series, we’ve covered the following parts: 

Part I: Project Setup, Data Modelling Process, and Testing. In part one of this series, we looked at an overview of the demo project that we’re building, set up the local environment, modeled our data and started testing it.

Part II: Integrating GraphQL with PostGraphile. In part two, we dove into some of the core aspects of GraphQL and implemented and tested the back-end features of our project.

You can find the source code for this project at: https://github.com/ScalablePath/full-stack-gql-ts-node-tutorial 

The final state of what we’re going to build in this part of the tutorial is available at: https://github.com/ScalablePath/full-stack-gql-ts-node-tutorial/tree/main/part-3/fe 

Project Recap: Building a Product Catalogue and Inventory Management Application

As a quick recap, we’re building a simple product catalog and inventory management application that:

  • Supports CRUD operations for Products, Categories, Subcategories, Units of Measure (UOMs), Suppliers & Warehouses 
  • Supports linking Products to Categories, Subcategories & UOMs
  • Supports linking Suppliers to Products with a many-to-many relationship
  • Tracks inventory transactions
  • Features stock management with support for multiple Warehouses

Previously, we implemented our application back-end, including data models defined asTypeORM entities with PostgreSQL as our database. We have also created our GraphQL API endpoints.

Next, we’ll focus on the front-end and integrate with our GraphQL API using Apollo Client and GraphQL codegen tools.Our goal is to build a sample full-stack app that showcases how to use and interact with a GraphQL API while having end-to-end type safety with TypeScript.

Here’s a sneak peek of what we are building:

Full Stack tutorial demo of an inventory management application

Let’s get started!

Building a Front-end Client with React

First, we’ll bootstrap our front-end with React & TypeScript and configure some additional dependencies.

Project Setup

Let’s use the standard create-react-app to set up our project with React + TypeScript:

1npx create-react-app graphql-fe --template typescript
2

We’ll also need include the following React Router for client-side routing:

1npm install react-router-dom@6.3.0
2

Now let’s add Tailwind UI for effortless and consistent styling of our user interface. To use Tailwind, we need to first install it as a PostCSS plugin and then we can easily integrate with Webpack:

1npx tailwindcss init -p
2

We should edit /src/index.css to replace it with the following statements:

1@tailwind base;
2@tailwind components;
3@tailwind utilities;
4

And finally, to make use of the tailwind/forms library that we installed, replace the contents of the tailwind.config.js with this:

1/** @type {import('tailwindcss').Config} */
2module.exports = {
3 content: [
4 "./src/**/*.{js,jsx,ts,tsx}"
5 ],
6 theme: {
7 extend: {},
8 },
9 plugins: [
10 require('@tailwindcss/forms')
11 ],
12}
13

Next, we need to install Apollo Client to our app in order to make the integration with our back-end work. 

Setting up Apollo Client with React

First, install Apollo Client and its dependencies:

1npm install @apollo/client@3.6.9 graphql@16.5.0
2npm install -—save-dev @types/graphql@14.5.0
3

Now, let’s configure it inside our front-end by updating the \src\index.tsx:

1import React from "react";
2import ReactDOM from "react-dom/client";
3import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
4
5import "./index.css";
6import App from "./App";
7import ErrorBoundary from "./ErrorBoundary";
8
9const client = new ApolloClient({
10 uri: 'http://localhost:8090/graphql',
11 cache: new InMemoryCache(),
12});
13
14const root = ReactDOM.createRoot(
15 document.getElementById('root') as HTMLElement
16);
17root.render(
18 <React.StrictMode>
19 <ApolloProvider client={client}>
20 <App />
21 </ApolloProvider>
22 </React.StrictMode>
23);
24

This code assumes that the back-end is up and running on localhost on port 8090. Please change it as needed.

Configuring CORS

To prevent potential CORS issues when running our app in the browser and integrating with our GraphQL API, we need to add some additional configurations in our back-end. 

To do this, let’s install these new packages in our back-end:

1npm install cors@2.8.5
2npm install --save-dev @types/cors@2.8.12
3

And edit the src\App.ts file to include the following lines:

1import cors from 'cors'
2
1 const app = express()
2 app.use(express.json())
3 app.use(cors()) // This needs to be added
4 app.use(postgraphile(`postgresql://${pgUser}@localhost/catalog_db`, 'public', {
5 watchPg: true,
6 graphiql: true,
7 enhanceGraphiql: true,
8 appendPlugins: [RegisterTransactionPlugin],
9 }))
10

Components Structure

We’re all set up to wire our app routes and create our UI components. To do this, create a /src/components folder with these files inside it:

Screenshot of UI components structure for Full-Stack App.

Let’s start with the /components/Categories.tsx. We’ll be using this component to serve as a template for the others, so some of the variable names will be more generic. You can think of this component as a simple boilerplate code for our demo purposes.

Our plan is to display a list of the existing records and the options to include new ones via a modal dialog. We should also be able to edit/delete those records. We’ll begin by importing the dependencies.

1import { useState } from "react";
2import { useQuery, useMutation, gql } from "@apollo/client";
3import { ModalDialog } from "./ModalDialog";
4

We’re going to import a ModalDialog component (I’ll demonstrate how it’s implemented shortly). 
Next, we need to define GraphQL queries and mutations by using gql literals.

Next, declare types that mimic the GraphQL schema for this particular entity with: 

Now we’re ready to define a variable to hold the name for this entity and create a React component called Categories.

1const entityName = "Category";
2
3export const Categories = (props: {}) => {}
4

Inside this React component, we’ll create some variables to manage the state using the useState hook. We’ll also use the useQuery and useMutation hooks to interact with the GraphQL API.

1const [displayModal, setDisplayModal] = useState(false);
2 const [entity, setEntity] = useState<Entity | undefined>(undefined);
3
4 // Usage of the the Apollo Client's useQuery & useMutation to interact with
5 // our GraphQL API
6 const { loading, error, data } = useQuery<AllEntity>(GET_ALL);
7 const [addEntity, { error: errorAdding }] = useMutation(ADD_ENTITY, {
8 refetchQueries: [{ query: GET_ALL }]
9 });
10 const [deleteEntity, { error: errorDeleting }] = useMutation(DELETE_ENTITY, {
11 refetchQueries: [{ query: GET_ALL }]
12 });
13 const [updateEntity, { error: errorUpdating }] = useMutation(UPDATE_ENTITY, {
14 refetchQueries: [{ query: GET_ALL }]
15 });
16

Don’t forget error handling! Here’s the boilerplate code we’ll be using to handle errors:

1if (loading) return <span>Loading...</span>;
2 if (error || errorAdding || errorDeleting || errorUpdating) {
3 const message =
4 error?.message ||
5 errorAdding?.message ||
6 errorDeleting?.message ||
7 errorUpdating?.message;
8 return <span>{`Error: ${message}`}</span>;
9 }
10 if (!data) return <span>No records found.</span>;
11

Let’s define a handleSave callbackfunction that will update the entity if it already exists or add a new one if it doesn’t:

1const handleSave = () => {
2 setDisplayModal(false);
3 // Verifies if it's an update operation or ir it should create a new entity
4 // based on having an existing nodeId
5 if (entity?.nodeId) {
6 updateEntity({
7 variables: {
8 nodeId: entity.nodeId,
9 description: entity.description
10 }
11 });
12 } else {
13 addEntity({ variables: { description: entity?.description } });
14 }
15 };
16

We’re almost there! Now, it’s time to define a function to render the existing entity data into a simple table.

1 // Renders the existing entity data into a simple table
2 const renderData = () => {
3 return data.allCategories.nodes.map((entity: Entity) => {
4 const { nodeId, id, description } = entity;
5 return (
6 <tr key={id}>
7 <td className="px-6 py-4 whitespace-nowrap">
8 <div className="text-sm text-gray-900">{id}</div>
9 </td>
10 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
11 {description}
12 </td>
13 <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
14 <button
15 className="text-indigo-600 hover:text-indigo-900 pr-2"
16 onClick={() => {
17 setEntity(entity);
18 setDisplayModal(true);
19 }}>
20 Edit
21 </button>
22 <button
23 className="text-indigo-600 hover:text-indigo-900"
24 onClick={() => deleteEntity({ variables: { nodeId } })}>
25 Delete
26 </button>
27 </td>
28 </tr>
29 );
30 });
31 };
32

Finally, we can define the markup that we will be returning from our component.

1return (
2 <>
3 {displayModal && (
4 <ModalDialog
5 title={`New ${entityName}`}
6 onClose={() => setDisplayModal(false)}
7 onSave={handleSave}
8 enableSave={!!entity?.description}
9 content={<EntityDetails entity={entity} setEntity={setEntity} />}
10 />
11 )}
12 <div className="flex flex-col">
13 <div className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
14 <button
15 className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
16 onClick={() => {
17 setEntity(undefined);
18 setDisplayModal(true);
19 }}>
20 New
21 </button>
22 </div>
23 <div className="py-2 align-middle inline-block min-w-full">
24 <div className="shadow overflow-hidden border-b border-gray-200">
25 <table className="min-w-full divide-y divide-gray-200">
26 <thead className="bg-gray-50">
27 <tr>
28 <th
29 scope="col"
30 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
31 ID
32 </th>
33 <th
34 scope="col"
35 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
36 Description
37 </th>
38 <th scope="col" className="relative px-6 py-3">
39 <span className="sr-only">Actions</span>
40 </th>
41 </tr>
42 </thead>
43 <tbody className="bg-white divide-y divide-gray-200">
44 {renderData()}
45 </tbody>
46 </table>
47 </div>
48 </div>
49 </div>
50 </>
51 );
52

Note that this markup is using a component called EntityDetails. The definition for this is:

1// Component that implements a visual representation of the entity details,
2// to be utilized inside of the ModalDialog instance.
3const EntityDetails = (props: {
4 entity: Entity | undefined;
5 setEntity: React.Dispatch<React.SetStateAction<Entity | undefined>>;
6}) => {
7 const { entity, setEntity } = props;
8 return (
9 <>
10 <div className="grid col-span-1 m-2 ">
11 <label
12 htmlFor="description"
13 className="form-label inline-block mb-2 ml-1">
14 Description
15 </label>
16 <input
17 type="text"
18 onChange={(e) =>
19 setEntity({ ...entity, description: e.target.value })
20 }
21 value={entity?.description || ""}
22 className="form-control block w-full rounded-lg mb-5"
23 id="description"
24 />
25 </div>
26 </>
27 );
28};
29

Now, let’s take a look at some of the key concepts that we introduced in this file.

Key Concepts

useQuery & useMutation

These hooks allow us to integrate with GraphQL’s read and write operations. We can use these Apollo Client hooks to track the loading/error states and interact with the responses whenever we’re querying or mutating an entity. You can find more details in Apollo GraphQL Documentation Queries and Mutations in Apollo Client.

gql

The gql construct makes use of a tagged template. It helps to write our GraphQL queries and mutations using a templated string.

Caching

There are many ways to tackle data caching in Apollo Client. It has a built-in robust caching mechanism that helps manage cached data. However, for the sake of simplicity, we are using data refetching instead of relying on a caching strategy. This is accomplished by specifying the refetechQueries parameter, as seen here:

1const [addEntity, { error: errorAdding }] = useMutation(ADD_ENTITY, {
2 refetchQueries: [{ query: GET_ALL }]
3 });
4 const [deleteEntity, { error: errorDeleting }] = useMutation(DELETE_ENTITY, {
5 refetchQueries: [{ query: GET_ALL }]
6 });
7 const [updateEntity, { error: errorUpdating }] = useMutation(UPDATE_ENTITY, {
8 refetchQueries: [{ query: GET_ALL }]
9 });
10

The refetchQueries options instructs Apollo Client to refetch the given query (GET_ALL in our case) whenever a successful mutation is executed.

It’s out of the scope of this article to dive into the available cache policies, custom cache updates and optimistic strategies. But if you’re interested in learning more about these topics, you can find more information here.

Simple Validation

It’s worth noting that our validation mechanism is very basic. In our modal, we are enabling the save option when the required fields are filled, as seen here:

1<ModalDialog
2 title={`New ${entityName}`}
3 onClose={() => setDisplayModal(false)}
4 onSave={handleSave}
5 enableSave={!!entity?.description}
6 content={<EntityDetails entity={entity} setEntity={setEntity} />}
7 />
8

For this particular example, we’re setting the enableSave prop to true only if the entity description is filled.

A more sophisticated validation could be relying on form validation tools like Formik.

Great. We’ve now defined how to integrate the front-end with our GraphQL API, and you can find the source code for the other components below. It’s worth noting that since we are using  /components/Categories.tsx as a template, these new components may not have any major difference, except for the visual changes regarding the field names displayed in the UI for each entity.

Source Code for All Other Components

We won’t include the source code for the other components in this tutorial, but you can find them in their final state inside the Github repo! Here are the links for each:

ModalDialog

This is how the shared ModalDialog component is implemented: /components/ModalDialog.tsx

1export const ModalDialog = (props: {
2 title: string;
3 content: JSX.Element;
4 enableSave: boolean;
5 onClose: () => void;
6 onSave: () => void;
7}) => {
8 const { title, content, enableSave, onClose, onSave } = props;
9
10 return (
11 <div className="fixed w-full h-full top-0 left-0 flex items-center justify-center">
12 <div className="relative p-4 w-full max-w-2xl h-full sm:h-auto">
13 <div className="relative bg-white rounded-lg shadow">
14 <div className="flex justify-between items-start p-4 rounded-t border-b">
15 <h3 className="text-xl font-semibold">{title}</h3>
16 <button
17 type="button"
18 className="text-gray-400 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:bg-gray-600 hover:text-white"
19 onClick={onClose}>
20 <svg
21 aria-hidden="true"
22 className="w-5 h-5"
23 fill="currentColor"
24 viewBox="0 0 20 20"
25 xmlns="http://www.w3.org/2000/svg">
26 <path
27 fillRule="evenodd"
28 d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
29 clipRule="evenodd"></path>
30 </svg>
31 <span className="sr-only">Close modal</span>
32 </button>
33 </div>
34 {content}
35 <div className="flex items-center justify-end p-3 space-x-2 rounded-b border-t border-gray-200 text-right">
36 <button
37 disabled={!enableSave}
38 className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
39 onClick={onSave}>
40 Save
41 </button>
42 </div>
43 </div>
44 </div>
45 </div>
46 );
47};
48

We’ll update the App.tsx file to look like this:

1import { BrowserRouter, Routes, Route } from "react-router-dom";
2
3import { Dashboard } from "./Dashboard";
4import { Categories } from "./components/Categories";
5import { Subcategories } from "./components/Subcategories";
6import { Suppliers } from "./components/Suppliers";
7import { Uoms } from "./components/Uoms";
8import { Warehouses } from "./components/Warehouses";
9import { Products } from "./components/Products";
10import { SupplierProducts } from "./components/SupplierProducts";
11import { Transactions } from "./components/Transactions";
12
13
14function App() {
15 return (
16 <BrowserRouter>
17 <Routes>
18 <Route path="/" element={<Dashboard />} />
19 <Route path="/categories" element={<Categories />} />
20 <Route path="/subcategories" element={<Subcategories />} />
21 <Route path="/suppliers" element={<Suppliers />} />
22 <Route path="/uoms" element={<Uoms />} />
23 <Route path="/warehouses" element={<Warehouses />} />
24 <Route path="/products" element={<Products />} />
25 <Route path="/supplier-products" element={<SupplierProducts />} />
26 </Routes>
27 </BrowserRouter>
28 );
29}
30
31export default App;
32

With this, we’re defining the demo app routes.

In addition, let’s create a Dashboard.tsx in the root of our /src directory with the following content:

1import { Link } from "react-router-dom";
2
3export const Dashboard = () => {
4 return (
5 <div className="grid grid-cols-1 gap-4 m-5">
6 <Link className="underline" to="/categories">
7 Categories
8 </Link>
9 <Link className="underline" to="/subcategories">
10 Subcategories
11 </Link>
12 <Link className="underline" to="/suppliers">
13 Suppliers
14 </Link>
15 <Link className="underline" to="/uoms">
16 Uoms
17 </Link>
18 <Link className="underline" to="/warehouses">
19 Warehouses
20 </Link>
21 <Link className="underline" to="/products">
22 Products
23 </Link>
24 <Link className="underline" to="/supplier-products">
25 Supplier Products
26 </Link>
27 </div>
28 );
29};
30
31

Testing Our Demo Application

It’s time for us to give it a try. The UI is really simplistic and focused on the CRUD operations. To start the app, just run this command:

1npm run start
2

You should be able to access the demo app on http://localhost:3000. To make sure there aren’t any errors, you should see the main page with the following links:

Demo app first screen of the full-stack tutorial

Each link redirects to a page containing a list of existing data and buttons for creating, editing and deleting records. Here’s the Categories page:

Category page of the full-stack tutorial demo app

You should also check to see that the back-end is running. Otherwise, you might see errors like Error: Failed to fetch.

Clicking the New button launches the modal dialog:

Modal dialog for adding new categories or editing the existing ones on the Full-stack application demo

Next Up: Improving Our Code with GraphQL Codegen

Our code is hard to maintain and scale at the moment because: 

  1. We don’t have type safety without relying on declaring types for the responses.
  2. We can’t have type validation when building the mutations/queries parameters.
  3. Fetching operations are not encapsulated in the sense that we are mixing everything in the same file.
  4. Error handling is repetitive.

Let’s explore ways to improve our code.

Codegen

GraphQL Codegen can be helpful to better organize our code and it’s particularly useful with TypeScript, so we can have real type safety mechanisms in place.

We’ll use the GraphQL Code Generator tool and install it with the following commands: 

1npm install -D @graphql-codegen/cli@^2.13.6
2npm install -D @graphql-codegen/fragment-matcher@3.3.1
3npm install -D @graphql-codegen/import-types-preset@^2.2.3
4npm install -D @graphql-codegen/introspection@2.2.1
5npm install -D @graphql-codegen/typescript@2.7.3
6npm install -D @graphql-codegen/typescript-operations@2.5.3
7npm install -D @graphql-codegen/typescript-react-apollo@3.3.3
8

After installing these dependencies, let’s create a file under the root directory of our project (the parent directory of the /src folder) called codegen.yml with the following content:

1overwrite: true
2schema: "http://localhost:8090/graphql"
3documents: "src/**/*.graphql"
4generates:
5 src/generated/graphql.tsx:
6 plugins:
7 - "typescript"
8 - "typescript-react-apollo"
9 - "fragment-matcher"
10 - "typescript-operations"
11 config:
12 flattenGeneratedTypes: true
13 skipTypename: true
14 ./graphql.schema.json:
15 plugins:
16 - "introspection"
17

And inside our package.json file let’s include this script in the scripts section:

1"generate": "graphql-codegen --config codegen.yml"
2

Now let’s create a file called Categories.graphql inside /src/components:

This file essentially contains the mutations and queries extracted from the gql literals inside of the Categories.tsx file.

Then run our recently added command:

1npm run generate
2

If everything runs as expected, you’ll notice that a src/generated/ directory is created and it contains the code that is generated by the codegen tools. From now on, every .graphql that we include in our /src/ folder will be included in our codegen generation process.

Now let’s start making use of these generated files! We’ll start by refactoring the Categories.tsx component. Since the other components share the same code structure, refactoring them should be a straightforward process. You just need to apply the same changes, including creating the .graphql file and using the correct imports.
In the /src/Categories.tsx file, add the following imports:

1import {
2 useGetCategoriesQuery,
3 CategoryPatch,
4 useAddCategoryMutation,
5 useDeleteCategoryMutation,
6 useUpdateCategoryMutation,
7 GetCategoriesDocument
8} from "../generated/graphql";
9

We also need to remove all of the existing gql usage, by removing the GET_ALL, ADD_ENTITY, DELETE_ENTITY and UPDATE_ENTITY declarations. Also, we don’t need to have the declaration of the AllEntity interface anymore. The Entity interface will be refactored to this:

1interface Entity extends CategoryPatch {
2 nodeId?: string;
3}
4

Things start to get more interesting when we replace this code section:

1 const { loading, error, data } = useQuery<AllEntity>(GET_ALL);
2 const [addEntity, { error: errorAdding }] = useMutation(ADD_ENTITY, {
3 refetchQueries: [{ query: GET_ALL }]
4 });
5 const [deleteEntity, { error: errorDeleting }] = useMutation(DELETE_ENTITY, {
6 refetchQueries: [{ query: GET_ALL }]
7 });
8 const [updateEntity, { error: errorUpdating }] = useMutation(UPDATE_ENTITY, {
9 refetchQueries: [{ query: GET_ALL }]
10 });
11

With this:

1 const { loading, error, data } = useGetCategoriesQuery();
2
3 const [addEntity, { error: errorAdding }] = useAddCategoryMutation({
4 refetchQueries: [{ query: GetCategoriesDocument }]
5 });
6 const [deleteEntity, { error: errorDeleting }] = useDeleteCategoryMutation({
7 refetchQueries: [{ query: GetCategoriesDocument }]
8 });
9 const [updateEntity, { error: errorUpdating }] = useUpdateCategoryMutation({
10 refetchQueries: [{ query: GetCategoriesDocument }]
11 });
12

It may look like a subtle change, but now we can have type safety out of the box. Whenever we add/remove new fields, our type definition changes will be reflected in our source code at compile time. 

Here’s the Categories.tsx with all of the changes that were introduced:

1import { useState } from "react";
2import { ModalDialog } from "./ModalDialog";
3import {
4 useGetCategoriesQuery,
5 CategoryPatch,
6 useAddCategoryMutation,
7 useDeleteCategoryMutation,
8 useUpdateCategoryMutation,
9 GetCategoriesDocument
10} from "../generated/graphql";
11
12const entityName = "Category";
13
14interface Entity extends CategoryPatch {
15 nodeId?: string;
16}
17
18export const Categories = (props: {}) => {
19 const [displayModal, setDisplayModal] = useState(false);
20 const [entity, setEntity] = useState<Entity | undefined>(undefined);
21
22 const { loading, error, data } = useGetCategoriesQuery();
23
24 const [addEntity, { error: errorAdding }] = useAddCategoryMutation({
25 refetchQueries: [{ query: GetCategoriesDocument }]
26 });
27 const [deleteEntity, { error: errorDeleting }] = useDeleteCategoryMutation({
28 refetchQueries: [{ query: GetCategoriesDocument }]
29 });
30 const [updateEntity, { error: errorUpdating }] = useUpdateCategoryMutation({
31 refetchQueries: [{ query: GetCategoriesDocument }]
32 });
33
34 // Boilerplate code for handling loading & error states
35 if (loading) return <span>Loading...</span>;
36 if (error || errorAdding || errorDeleting || errorUpdating) {
37 const message =
38 error?.message ||
39 errorAdding?.message ||
40 errorDeleting?.message ||
41 errorUpdating?.message;
42 return <span>{`Error: ${message}`}</span>;
43 }
44 if (!data?.allCategories) return <span>No records found.</span>;
45
46 const handleSave = () => {
47 setDisplayModal(false);
48 // Verifies if it's an update operation or ir it should create a new entity
49 // based on having an existing nodeId
50 if (entity?.nodeId) {
51 updateEntity({
52 variables: {
53 nodeId: entity.nodeId,
54 description: entity.description!
55 }
56 });
57 } else {
58 addEntity({ variables: { description: entity?.description! } });
59 }
60 };
61
62 // Renders the existing entity data into a simple table
63 const renderData = () => {
64 return data.allCategories!.nodes.map((entity) => {
65 if (!entity) return null
66 const { nodeId, id, description } = entity;
67 return (
68 <tr key={id}>
69 <td className="px-6 py-4 whitespace-nowrap">
70 <div className="text-sm text-gray-900">{id}</div>
71 </td>
72 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
73 {description}
74 </td>
75 <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
76 <button
77 className="text-indigo-600 hover:text-indigo-900 pr-2"
78 onClick={() => {
79 setEntity(entity!);
80 setDisplayModal(true);
81 }}>
82 Edit
83 </button>
84 <button
85 className="text-indigo-600 hover:text-indigo-900"
86 onClick={() => deleteEntity({ variables: { nodeId } })}>
87 Delete
88 </button>
89 </td>
90 </tr>
91 );
92 });
93 };
94
95 return (
96 <>
97 {displayModal && (
98 <ModalDialog
99 title={`New ${entityName}`}
100 onClose={() => setDisplayModal(false)}
101 onSave={handleSave}
102 enableSave={!!entity?.description}
103 content={<EntityDetails entity={entity} setEntity={setEntity} />}
104 />
105 )}
106 <div className="flex flex-col">
107 <div className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
108 <button
109 className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
110 onClick={() => {
111 setEntity(undefined);
112 setDisplayModal(true);
113 }}>
114 New
115 </button>
116 </div>
117 <div className="py-2 align-middle inline-block min-w-full">
118 <div className="shadow overflow-hidden border-b border-gray-200">
119 <table className="min-w-full divide-y divide-gray-200">
120 <thead className="bg-gray-50">
121 <tr>
122 <th
123 scope="col"
124 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
125 ID
126 </th>
127 <th
128 scope="col"
129 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
130 Description
131 </th>
132 <th scope="col" className="relative px-6 py-3">
133 <span className="sr-only">Actions</span>
134 </th>
135 </tr>
136 </thead>
137 <tbody className="bg-white divide-y divide-gray-200">
138 {renderData()}
139 </tbody>
140 </table>
141 </div>
142 </div>
143 </div>
144 </>
145 );
146};
147
148// Component that implements a visual representation of the entity details,
149// to be utilized inside of the ModalDialog instance.
150const EntityDetails = (props: {
151 entity: Entity | undefined;
152 setEntity: React.Dispatch<React.SetStateAction<Entity | undefined>>;
153}) => {
154 const { entity, setEntity } = props;
155 return (
156 <>
157 <div className="grid col-span-1 m-2 ">
158 <label
159 htmlFor="description"
160 className="form-label inline-block mb-2 ml-1">
161 Description
162 </label>
163 <input
164 type="text"
165 onChange={(e) =>
166 setEntity({ ...entity, description: e.target.value })
167 }
168 value={entity?.description || ""}
169 className="form-control block w-full rounded-lg mb-5"
170 id="description"
171 />
172 </div>
173 </>
174 );
175};
176

Whenever we define new custom GraphQL queries/mutations or update the existing ones, remember to run the generate script again so the codegen tool can update the definitions file.

It’s important to be aware that the codegen tool creates type definitions for the custom GraphQL definitions described in the .graphql files. But since it connects to our GraphQL schema definition in the back-end, it also supports all of the existing queries and mutations. 

Error Handling with Error Boundaries in React

Oftentimes dealing with error handling is a tedious task. If we look at our existing code, we can see lots of repetitive code to manage error cases. One simple way to enhance this is to use Error Boundaries in React. It may serve as an escape with a fallback UI in case an error is not handled in a component.

The one that we are going to create next is a custom Error Boundary for our demo. The purpose of it is just to simply log the error in the console and display a message to the user.
Let’s create it in /src/ErrorBoundary.tsx:

1import { Component } from "react";
2
3export default class ErrorBoundary extends Component {
4 constructor(props) {
5 super(props);
6 this.state = { hasError: false };
7 }
8
9 static getDerivedStateFromError(_error) {
10 return { hasError: true };
11 }
12
13 componentDidCatch(error, errorInfo) {
14 // Something went wrong, logs the error.
15 console.log(`An unexpected error has happened: ${errorInfo}`, error);
16 }
17
18 render() {
19 // Display an error message to the user when an error occurs.
20 if (this.state.hasError) {
21 return (
22 <h1>Something went wrong! Please refresh the page and try again.</h1>
23 );
24 }
25
26 return this.props.children;
27 }
28}
29

And import it on our src/index.tsx file:

1import ErrorBoundary from "./ErrorBoundary"
2

And update this code section to make use of it:

1 <React.StrictMode>
2 <ErrorBoundary>
3 <ApolloProvider client={client}>
4 <App />
5 </ApolloProvider>
6 </ErrorBoundary>
7 </React.StrictMode>
8

Let’s create a simple custom Error class in src/NotFoundError.ts:

1export class NotFoundError extends Error {
2 constructor() {
3 super("Could not retrieve entity information.");
4 }
5}
6

Finally, let’s adapt our Categories.tsx file by importing our recently created custom error NotFoundError:

1import { NotFoundError } from "../NotFoundError";
2

And replacing this:

1 if (error || errorAdding || errorDeleting || errorUpdating) {
2 const message =
3 error?.message ||
4 errorAdding?.message ||
5 errorDeleting?.message ||
6 errorUpdating?.message;
7 return <span>{`Error: ${message}`}</span>;
8 }
9

With this:

1if (error || errorAdding || errorDeleting || errorUpdating) {
2 throw new NotFoundError();
3 }
4

Now, whenever we see an error when using our mutations/queries, we’re going to throw our custom error and it will be caught by our Error Boundary. 

Additional Front-end features 

We could showcase additional interesting features for our front-end, but to keep the project simple, we won’t cover them in this tutorial. As a quick overview of those features, we could include:

  • Usage of React Suspense to handle loading states
  • Prefetching data
  • Cache Resolvers
  • Lazy loading queries
  • Polling & Subscriptions
  • Apollo Link

Implementing Transactions Component to Finalize the Full-stack App

Let’s add the last few missing features in our app: registering inventory transactions and displaying those records together with the warehouse stocks. We’re going to prepare the related queries/mutation for our new Transactions component that we’re going to create next

We’ll start by specifying our GraphQL queries and mutations in a file called /src/components/Transactions.graphql:

And we’ll also organize the hooks to support those operations for our codegen to generate the components for us.

You may have noticed that we created two fragment declarations: StockData & TransactionData. Consider GraphQL fragments as a data subset that can be used by multiple mutations and queries. A common practice is to avoid repeating GraphQL declarations for commonly used attributes of a given type. For an in-depth explanation of how this works, please refer to the official document about fragments.

Although we’re not sharing these fragments among different queries/mutations yet, we could use them in the future if needed.

Before we run the generate script, let’s also add some new queries to fetch the Products and Warehouses that we need for our new Transactions component. Technically, we could define those in the same .graphql file, but it would be maintainable to organize them by entity type. 

With that, let’s create a /components/Products.graphql with this content:

Let’s create /components/Warehouses.graphql with this:

Let’s run the generate script to have code generation for our new GraphQL operations:

1npm run generate
2

We now have everything to introduce the src/components/Transactions.tsx component. Once we wire this component, it should look like this:

Screenshot of the Transactions component for the Full-Stack app demo

You can find the implementation for Transactions in the Github repo:  https://github.com/ScalablePath/full-stack-gql-ts-node-tutorial/blob/main/part-3/fe/src/components/Transactions.tsx
We need to register this new component in our Dashboard and routes. To do that, simply update the \src\App.tsx file by adding the following import:

1import { Transactions } from "./components/Transactions";
2

And by adding the route for Transactions:

1<Route path="/transactions" element={<Transactions />} />
2

In the \src\Dashboard.tsx, add the following link to the end:

1 <Link className="underline" to="/transactions">
2 Transactions
3 </Link>
4

Now, if you browse our dashboard route, you’ll see a new “Transactions” link. Feel free to play around with it!

Wrap-up and Additional Resources: Building GraphQL APIs

In this article series, we walked through building a back-end and front-end of a demo application to explore GraphQL APIs and client architecture that includes:

  • Type safety with TypeScript on both back-end and front-end
  • A maintainable API that can evolve easily
  • Data overfetching mitigation & multiple API call prevention when working with nested data structures.
  • Out of the box documentation of resources with GraphQL Playground

While we opted to use PostGraphile in the back-end and a codegen tool in our front-end, this is just one combination of the many options available to build and consume a GraphQL API. Given the scope of our demo app and to keep things simple, we’ve selected these tools to aid our development. 

With the architecture that we have implemented, adding new attributes or creating new entities is just as simple as modifying or creating TypeORM entities in the back-end. Once we do so, our changes will be automatically exposed in the API. The front-end will need to update the GraphQL schema via codegen and things will be available to use without having to do any additional work!


Looking to hire?

Join our newsletter

Join thousands of subscribers already getting our original articles about software design and development. You will not receive any spam. just great content once a month.