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:

app.js#
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:

package.json#
{
  "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#

Endpoint#
GET /api/invoice
Example request body#
N/A
Example response#
[
  { "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#

Endpoint#
POST /api/invoice
Example request body#
{
  "description": "Exterior paint",
  "rate": 20
}
Example response#
{
  "id": 4,
  "description": "Exterior paint",
  "rate": 20,
  "hours": 0
}

Update an invoice row#

Endpoint#
PUT /api/invoice/:id
PUT /api/invoice/4
Example request body#
{
  "hours": 1
}
Example response#
{
  "id": 4,
  "description": "Exterior paint",
  "rate": 20,
  "hours": 1
}

Delete an invoice row#

Endpoint#
DELETE /api/invoice/:id/delete
DELETE /api/invoice/3/delete
Example request body#
N/A
Example response#
{
  "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#

  1. Copy the TEST_DATA array from src/App.jsx to app.js.

    app.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}`));
    
  2. The GET /api/invoice route just needs to respond with TEST_DATA.

    app.js#
    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.

Postman Request Section

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.

Postman Response Section

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.

app.js#
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.js#
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}.

Postman POST request

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.

  1. Following the example of using route parameters from the Express docs, Express will store the value of :id in req.params.id. Add the code below to app.js to destructure id from req.params along with values from req.body:

    app.js#
    app.put('/api/invoice/:id', (req, res) => {
      const { id } = req.params;
      const { description, rate, hours } = req.body;
    
  2. Now you can use id to get the specified invoice row from TEST_DATA, update it according to the values from req.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.js#
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.js#
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.

  1. Import axios and wrap the code that renders your React app in the then() 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>,
      );
    });
    
  2. Pass response.data to App as a prop named initialInvoiceList (the example below uses parameter destructuring to get the value of response.data):

    src/main.jsx#
    axios.get('/api/invoice').then(({ data }) => {
      ReactDOM.createRoot(document.getElementById('root')).render(
        <React.StrictMode>
          <App initialInvoiceList={data} />
        </React.StrictMode>,
      );
    });
    
  3. Over in src/App.jsx, update the App component to replace TEST_DATA with initialInvoiceList from props:

    src/App.jsx#
    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.

Updated code for addInvoiceRow#
const addInvoiceRow = async () => {
  const { data } = await axios.post('/api/invoice', { description: 'Description' });

  const newInvoice = { ...data, isEditing: true };
  setInvoiceList([...invoiceList, newInvoice]);
};
Updated code for deleteInvoiceRow#
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);
  }
};
Adding the ID to the initialInvoiceData#
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.

Updated code for setNormalMode#
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.