Code Along: Building an Express Back End for React#
This is a continuation of the Thinking in React code along, where you were introduced to the idea of thinking in React and how to approach building a React app from scratch. Now, you’ll implement an Express back end and integrate it with the React front end.
What you’re building#
The React app you completed earlier is almost fully functional except that it uses static, mock data instead of dynamic data from a back-end service. You’ll use Express to build that service: a JSON API.
Getting started#
The starter code for this project is the solution code from Code Along: Thinking in React. You’re encouraged to continue working with that project.
If you’d rather start a new project or build off the “official” starter code, you can download it with the command below:
$ dmget wb-thinking-react-2
If you do decide to use the wb-thinking-react-2
starter, don’t forget that you’ll have to install
its dependencies and initialize Git for the project folder.
Set up Express#
The first task is to set up an Express app that’ll be able to talk to the existing React app.
Besides the typical Express dependencies (express
, morgan
, and nodemon
), you’ll also need to
install vite-express
. Run the command below to add these dependencies to package.json
and
install them:
$ npm install express morgan nodemon vite-express
Then, create an empty folder called server
in the project root. Inside server
, make a file
called app.js
with the following contents:
import express from 'express';
import morgan from 'morgan';
import ViteExpress from 'vite-express';
const app = express();
const port = '8000';
app.use(morgan('dev'));
app.use(express.urlencoded({ extended: false }));
ViteExpress.config({ printViteDevServerHost: true });
// Routes go here
ViteExpress.listen(app, port, () => console.log(`Server is listening on http://localhost:${port}`));
Instead of using npm run dev
to run Vite, ViteExpress
will handle that for you. It’ll run when
app.js
is executed, so you’ll need to replace the dev
script in package.json
so it runs
app.js
with nodemon
instead of running vite
:
{
"name": "wb-thinking-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "nodemon server/app.js -w server/app.js -w server -w src",
"vite-dev": "vite",
"build": "vite build",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
/* (...excerpt) */ }
Now you can run the development server with the dev
script:
$ npm run dev
If everything’s set up correctly, you should see the React app from Code Along: Thinking in React
when you navigate to localhost:8000
in your browser.
Plan endpoints#
A complete JSON API for the invoice data should allow API users to:
List all invoice data
Create a new invoice row
Update an invoice row
Delete an invoice row
Before you start writing any Express code, come up with a plan for how each feature should be implemented. One way to do this is to sketch out the endpoint, example request body(s), and example response(s) for each feature.
List all invoice data#
GET /api/invoice
N/A
[
{ "id": 0, "description": "Content plan", "rate": 50, "hours": 4 },
{ "id": 1, "description": "Copy writing", "rate": 50, "hours": 2 },
{ "id": 2, "description": "Website design", "rate": 50, "hours": 5 },
{ "id": 3, "description": "Website development", "rate": 100, "hours": 5 },
]
Create a new invoice row#
POST /api/invoice
{
"description": "Exterior paint",
"rate": 20
}
{
"id": 4,
"description": "Exterior paint",
"rate": 20,
"hours": 0
}
Update an invoice row#
PUT /api/invoice/:id
PUT /api/invoice/4
{
"hours": 1
}
{
"id": 4,
"description": "Exterior paint",
"rate": 20,
"hours": 1
}
Delete an invoice row#
DELETE /api/invoice/:id/delete
DELETE /api/invoice/3/delete
N/A
{
"id": 3
}
The HTTP PUT and DELETE request types
The PUT and DELETE request types are not used nearly as often as GET and POST, but it’s good to be aware of them, so we’ll use them here. When PUT and DELETE are used, the convention is to use PUT for updating data and DELETE for deleting data.
Both PUT and DELETE work similarly to a POST request, in that all data (if any) is sent in the request body. There is no query string.
List all invoice data#
Let’s start with the easy one: the endpoint for listing all invoice data.
Implement GET /api/invoice
#
Copy the
TEST_DATA
array fromsrc/App.jsx
toapp.js
.// (...excerpt) ViteExpress.config({ printViteDevServerHost: true }); const TEST_DATA = [ { id: 0, description: 'Content plan', rate: 50, hours: 4 }, { id: 1, description: 'Copy writing', rate: 50, hours: 2 }, { id: 2, description: 'Website design', rate: 50, hours: 5 }, { id: 3, description: 'Website development', rate: 100, hours: 5 }, ]; // Routes go here ViteExpress.listen(app, port, () => console.log(`Server is listening on http://localhost:${port}`));
The
GET /api/invoice
route just needs to respond withTEST_DATA
.app.get('/api/invoice', (req, res) => { res.json(TEST_DATA); });
Test the endpoint with Postman#
We could start integrating this with the React app, but the API should work even if React front end doesn’t exist. You should be able to manually test that the route works without using React.
One way to do this is to use special HTTP clients made specifically for developers (some, like
curl
or wget
, might already be installed on your OS). These applications can be either graphical user
interfaces or command line tools. One common HTTP client that is easy to learn and use is
Postman.
Postman is a relatively simple desktop application. You don’t need to install it on the command line or do any setup, just download it and open it on your computer. You shouldn’t need to sign up or create an account; the simplest version (the “Lightweight API Client”) should be more than good enough.
Once you open Postman, you should see two sections. The top section has everything to do with the request. You can change what type of request you’re making, specify the URL you’re making the request to, and add a body if needed.
In the response section on the bottom, you can see the data returned to you, as well as other details like any cookies or headers.
Check that your dev server is still running. Enter the following URL in the request bar, and make sure the request type is set to GET:
localhost:8000/api/invoice
In the response section on the bottom, you should see the data from TEST_DATA
. Success!
Create a new invoice row#
We want users of the API to be able to specify the description, rate, and hours when creating a new
invoice row. Otherwise, default values will be used. Like before, the ID will be generated with
the utility function in utils/idGenerator.js
.
Add the express.json()
middleware#
The express.json
middleware will do the work of parsing data from req.body
into a JavaScript
object for us.
import express from 'express';
import morgan from 'morgan';
import ViteExpress from 'vite-express';
import generateId from '../src/utils/idGenerator.js';
const app = express();
const port = '8000';
app.use(morgan('dev'));
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
ViteExpress.config({ printViteDevServerHost: true });
// (...excerpt)
Import generateID
#
You’ll also need to import the generateId
utility function at the top of app.js
:
import generateId from '../utils/idGenerator.js';
Implement POST /api/invoice
#
Now you have all the parts you need to implement the route for POST /api/invoice
.
app.post('/api/invoice', (req, res) => {
const { description, rate, hours } = req.body;
const newItem = {
id: generateId(),
// If no value is provided in req.body, use default values
description: description || '',
rate: Number(rate) || 0,
hours: Number(hours) || 0,
};
TEST_DATA.push(newItem);
res.json(newItem);
});
Test the endpoint#
To test the endpoint, use Postman to make a POST
request to the endpoint. You will need to send
a body for this request. To create the body:
Click the word “Body” in the top request section.
Click the radio button for “raw”.
There should be a dropdown that currently shows “Text”. Click the dropdown and select “JSON.”
In the text editor, enter the following JSON object:
{"description": "hello", "rate": 200}
.
Then press Send. Since you didn’t give a value for hours
, the route should use the default value
0
, resulting in the response:
{
"id": 4,
"description": "hello",
"rate": 200,
"hours": 0
}
Let’s check that the new invoice row has actually been added to the list by making a request to
GET /api/invoice
. If everything’s working, you should see the new invoice row included in the response.
Edit an invoice row#
This endpoint should allow API users to edit description, rate, and hours for an existing invoice row.
Implement PUT /api/invoice/:id
#
The exact path for this endpoint can have different values for :id
—if users want to update an
invoice with ID 3
, they need to make a request to /api/invoice/3
. Express can capture values
like :id
from a URL using route parameters.
Following the example of using route parameters from the Express docs, Express will store the value of
:id
inreq.params.id
. Add the code below toapp.js
to destructureid
fromreq.params
along with values fromreq.body
:app.put('/api/invoice/:id', (req, res) => { const { id } = req.params; const { description, rate, hours } = req.body;
Now you can use
id
to get the specified invoice row fromTEST_DATA
, update it according to the values fromreq.body
, and send the updated item in a JSON response:app.post('/api/invoice/:id', (req, res) => { const { id } = req.params; const { description, rate, hours } = req.body; const index = TEST_DATA.findIndex((item) => item.id === Number(id)); const item = TEST_DATA[index]; // Only update the values that are provided in req.body item.description = description || item.description; item.rate = Number(rate) || item.rate; item.hours = Number(hours) || item.hours; res.json(item); });
Test the endpoint#
Test what you have so far with Postman. Make a PUT request to the URL localhost:8000/api/invoice/1
,
with the body { "description": "edited", "rate": 0 }
.
It looks like the route works, but what if we try to edit a row that doesn’t exist? Try changing
the URL to use an ID that obviously doesn’t exist, e.g. localhost:8000/api/invoice/100
.
Notice that the server responded with HTML. This isn’t user-friendly—developers expect JSON APIs to respond with JSON, not HTML! Let’s add some error-handling to make this endpoint more usable.
Add error handling#
Update the handler function so the route responds with an error message if users try to edit an invoice row that doesn’t exist:
app.put('/api/invoice/:id', (req, res) => {
const { id } = req.params;
const { description, rate, hours } = req.body;
const index = TEST_DATA.findIndex((item) => item.id === Number(id));
if (index === -1) {
res.status(404).json({ error: `Item with ID ${id} not found.` });
} else {
const item = TEST_DATA[index];
// Only update the values that are provided in req.body
item.description = description || item.description;
item.rate = Number(rate) || item.rate;
item.hours = Number(hours) || item.hours;
res.json(item);
}
});
Try sending the PUT request via Postman to /api/invoice/100
again, and confirm that you
get a JSON response.
Delete an item#
Deleting an item is similar to editing an item except that the route won’t need any data from
req.body
. Also, instead of responding with a complete invoice row object, it’ll just respond with
the ID of the item that was deleted.
Implement DELETE /api/invoice/:id/delete
#
Just like with editing an invoice row, you’ll need to handle the error when users try to delete an item that doesn’t exist.
app.delete('/api/invoice/:id/delete', (req, res) => {
const { id } = req.params;
const index = TEST_DATA.findIndex((item) => item.id === Number(id));
if (index === -1) {
res.status(404).json({ error: `Item with ID ${id} not found.` });
} else {
TEST_DATA.splice(index, 1);
res.json({ id: Number(id) });
}
});
Test the endpoint#
Test the endpoint with Postman. Use the DELETE request type and the URL localhost:8000/api/invoice/3/delete
.
(No body is needed because you don’t need to send any data).
To check that the item was actually deleted, make another Postman request to GET /api/invoice
and check that the object was removed.
Integrating API endpoints with React#
Now that you’ve finished implementing your JSON API, the next step is to integrate it with the React app.
Install additional dependencies#
You’ll need Axios to make requests from your React app:
$ npm install axios
Replace TEST_DATA
with props#
Currently, the App
component in src/App.jsx
uses TEST_DATA
to pass initial values to
InvoiceTable
. Instead of using TEST_DATA
, you’ll want to make a request to the Express back end.
One way to do this is to request data before the initial render and pass it to the App
component
as props. The app’s initial render is triggered by the call to render()
in src/main.jsx
, so
let’s start there.
Import
axios
and wrap the code that renders your React app in thethen()
callback:import axios from 'axios'; import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './index.css'; axios.get('/api/invoice').then(() => { ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode>, ); });
Pass
response.data
toApp
as a prop namedinitialInvoiceList
(the example below uses parameter destructuring to get the value ofresponse.data
):axios.get('/api/invoice').then(({ data }) => { ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App initialInvoiceList={data} /> </React.StrictMode>, ); });
Over in
src/App.jsx
, update theApp
component to replaceTEST_DATA
withinitialInvoiceList
from props:import './App.css'; import InvoiceTable from './components/InvoiceTable.jsx'; function App({ initialInvoiceList }) { return <InvoiceTable initialInvoiceList={initialInvoiceList} />; } export default App;
Update functions that set state#
To get the JSON API integrated with everything else, you’ll need to update
addInvoiceRow
and deleteInvoiceRow
in src/components/InvoiceTable.jsx
. You’ll also
need to add the ID to the initialInvoiceData
in the map
expression just below those
functions, so that we can use it in the Axios call.
const addInvoiceRow = async () => {
const { data } = await axios.post('/api/invoice', { description: 'Description' });
const newInvoice = { ...data, isEditing: true };
setInvoiceList([...invoiceList, newInvoice]);
};
const deleteInvoiceRow = async (id) => {
const { data } = await axios.delete(`/api/invoice/${id}/delete`);
if (!data.error) {
const newInvoiceList = [...invoiceList];
const index = newInvoiceList.findIndex((invoice) => invoice.id === data.id);
newInvoiceList.splice(index, 1);
setInvoiceList(newInvoiceList);
}
};
const rows = invoiceList.map(({ id, description, rate, hours, isEditing }) => (
<InvoiceTableRow
key={id}
initialInvoiceData={{ id, description, rate, hours }}
initialIsEditing={isEditing}
onDeleteRow={() => deleteInvoiceRow(id)}
/>
));
Then, update setNormalMode
in src/components/InvoiceTableRow.jsx
.
const setNormalMode = async () => {
const { data } = await axios.put(`/api/invoice/${initialInvoiceData.id}`, {
description,
rate,
hours,
});
if (!data.error) {
setDescription(data.description);
setRate(data.rate);
setHours(data.hours);
}
setIsEditing(false);
};
Don’t forget to import Axios at the top of both these files.