Developing Full Stack Next.js Web Applications

Chapter 5 — Implementing RESTful Web APIs with Express.js

Dr. Jose Annunziato

During the 1990s, the adoption of the World Wide Web grew exponentially. A variety of commercial ventures explored numerous use cases, revolutionizing interactions between companies, their customers, and other businesses. Integration points between businesses are often referred to as business-to-business (B2B) interactions. Interactions between businesses and customers are commonly known as business-to-consumer (B2C) interactions. Many companies have largely automated customer interactions by implementing online storefronts where customers can browse products, place orders, submit reviews, and process returns without standing at a counter.

Creating visually appealing user interfaces is essential to capture customer attention, encourage purchases through marketing ads, and build long-term relationships through incentives like discounts and loyalty programs. User interfaces, as the name suggests, focus on application aspects that interact with users through visually engaging representations of data. Up to this point these interfaces have used hard-coded JSON files, such as courses.json and modules.json, to render data. Interfaces have been built to render and manipulate this data, updating the screen to reflect changes. Chapter 4 taught Kambaz to add, edit, and delete courses and modules in the browser, so the Dashboard and Modules screens felt like a working product.

However, these updates have not been permanent; refreshing the browser results in lost changes and a reset application state. JavaScript applications running on clients like browsers, game consoles, or TV boxes have limited options for retrieving and storing data permanently. A client cannot be the permanent store: it can crash, it can be closed, and it cannot be trusted with every piece of logic a business owns. The next chapters address the challenges of retrieving, storing, updating, and deleting data permanently on remote servers and databases from React applications.

This chapter builds the other half of the system: an Express.js HTTP server in a sibling project named webdev-server — not inside the Next.js app directory. Locally the UI is next dev on port 3000 and Express is nodemon / npm start on port 4000, with NEXT_PUBLIC_HTTP_SERVER=http://localhost:4000. §5.5 deploys that same server to Render (or Heroku); you do not need a remote host for the labs. axios talks to Express from the React screens. §5.3 adds App Router Route Handlers as a same-app alternative — a section, not the chapter spine. MongoDB is Chapter 6; this chapter keeps the collections in process memory so the URLs and clients can stay the same when the database arrives.

5.1 Installing and Configuring an HTTP Web Server

Slides

The Kambaz React Web application built so far is the client in a client/server architecture. Users interact with client applications that implement user interfaces relying on servers to store data and execute complex logic that would be impractical on the client. Clients and servers interact through a sequence of requests and responses. Clients send requests to servers, servers execute some logic, fulfill the request, and then respond back to the client with results. This section discusses implementing HTTP servers using Node.js.

Glance at the Lab 5 checklist as you go, then confirm coverage after you have built each sample — §5.2 is the walkthrough, not a skip list. Open http://localhost:3000/labs/lab5 once the companion server is running so the LiveDemos can reach http://localhost:4000.

5.1.1 Introduction to Node.js

JavaScript is generally recognized as a programming language designed to execute in browsers, but it has been rescued from its browser confines by Node.js. Node.js is a JavaScript runtime that interprets and executes applications written in JavaScript outside of browsers, such as from a desktop console or terminal. This allows JavaScript applications written for the desktop to overcome many limitations faced by those in the browser.

JavaScript running in a browser is restricted, with no access to the filesystem or databases, and limited network capabilities. In contrast, JavaScript running on a desktop has full access to the filesystem, databases, and unrestricted network access. Conversely, desktop JavaScript applications generally lack a user interface and offer limited user interaction, while browser-based JavaScript applications provide rich and sophisticated interfaces for user interaction. That split is why this chapter keeps React in the browser and moves storage and request handling into a Node process: each runtime does the work it is good at.

5.1.2 Installing Node.js

Node.js is a JavaScript runtime that can execute JavaScript on a desktop, allowing JavaScript programs to break out from the confines and limitations of a browser. Node.js was installed during previous chapters while implementing the React Web application. Confirm the installation and check the version by typing the following in your computer terminal or console application.

node -v
# v22.11.0

If a Node installation is present, its version will be displayed on the console; otherwise, an error message will be shown, indicating that Node.js needs to be downloaded and installed from nodejs.org. As of this writing, Node.js 22.11.0 was a current long-term support release, but any version recommended on Node's website can be installed. Once downloaded, double-click the installer, give the operating system the permissions it requests, accept the defaults, let the installer complete, and restart the computer if the installer asks. Once the computer is up and running again, confirm Node.js installed properly by running node -v again from the command line.

5.1.3 Creating a Node.js Project

Another tool installed along with Node.js is npm — Node Package Manager — which has been used thus far to run React applications in previous chapters. The npm command can also install and execute Node.js packages on the local computer as well as create brand new Node.js projects. To create a Node.js project, create a directory with the name of the desired project and then change into that directory as shown below. Choose a directory name that does not contain any spaces, is all lowercase, and uses dashes between words.

NOTE: DO NOT create the Node.js project directory inside the existing Next.js React project directory. The Next.js React project should be in a directory called webdev-client (or similar) and the new webdev-server directory SHOULD NOT be inside the Next.js React project directory. Instead, the two directories should be siblings — they should have the same parent directory.

mkdir webdev-server
cd webdev-server
npm init

Again: do not nest the server inside the Next.js tree. Once in the Node.js project directory, npm init kicks off an interactive session asking details about the project such as the name of the project and the author. Each question provides a default answer which can be accepted or skipped by just pressing enter. It is fine to initially keep all the default values since they can be configured later. A sample interaction looks like this:

package name: (webdev-server)
version: (1.0.0)
description: Node.js HTTP Web server for the Kambaz application
entry point: (index.js)
test command:
git repository: https://github.com/<you>/webdev-server
keywords: Node, REST, Server, HTTP, Web Development
author: Jose Annunziato
license: (ISC)

The configuration will be written into a new file called package.json in a JSON format and it is distinctive of Node.js projects, the way a pom.xml might be distinctive for Java projects. This interactive book ships a working copy at the repo root as webdev-server/ so LiveDemos can call http://localhost:4000 without cloning a second remote. It is still a separate project: own package.json, own .gitignore, own README. For Canvas you git init that folder and push a second GitHub repository named webdev-server (§5.5.1) — do not treat it as Next.js app source.

5.1.4 Creating a Simple Hello World Node.js Program

Open the Node.js project directory created earlier with an IDE such as Visual Studio Code or IntelliJ, and at the root of the project, create a JavaScript file called Hello.js with the content shown below. The script uses the console.log() function to print the string Hello World! to the console and it is a common first program to write when learning a new language or infrastructure.

Hellowebdev-server/Hello.js
console.log("Hello World!");

At the command line, run the Hello.js application by using the node command and confirm the application prints Hello World! to the console as shown below.

node Hello.js
# Hello World!

Node.js programs consist of JavaScript files that are executed with the node command-line interpreter. The following sections describe writing JavaScript applications that implement HTTP Web servers and RESTful Web APIs to integrate with React user interfaces. Upcoming chapters describe writing JavaScript applications that store and retrieve data from databases such as MongoDB. Later files turn this hello script into HTTP routes; the file name stays because we will export a function from it.

5.1.5 Creating a Node.js HTTP Web Server

Express is a very popular Node.js library that simplifies creating HTTP servers and Web APIs. Express HTTP servers can respond to HTTP requests from HTTP clients such as the React user interface implemented in earlier chapters. From the root directory of the Node.js project, install the express library from the terminal as shown below so it is listed in package.json:

npm install express

Confirm that an express entry appears in package.json in the dependencies property. It is important these dependencies are listed in package.json so that they can be re-installed by other colleagues or when deploying to remote servers and cloud platforms such as AWS, Heroku, or Render. New libraries are installed in a new folder called node_modules. More Node.js packages can be found at npmjs.com.

The following index.js implements an HTTP server that responds Hello World! when the server receives an HTTP request at the URL http://localhost:4000/hello. Copy and paste the URL in a browser to send the HTTP request and the browser will render the response from the server. The require function is equivalent to the import keyword and loads a library into the local source — we start with require so you see the older Node style, then §5.1.7 switches the project to ES modules. The express() function call creates an instance of the express library and assigns it to a local constant app. The app instance is used to configure the server on what to do when various types of requests are received. For instance the example below uses the app.get() function to configure an HTTP GET request handler by mapping the URL pattern /hello to a function that handles the HTTP request.

indexwebdev-server/index.js
import express from "express";
const app = express();
app.get("/hello", (req, res) => {
  res.send("Hello World!");
});
app.listen(4000);

A request to URL http://localhost:4000/hello triggers the function implemented in the second argument of app.get(). The handler function receives parameters req and res which allow the function to participate in the request/response interaction, common in client/server applications. The res.send() function responds to the request with the text Hello World! Use node to run the server from the root of the project as shown below.

node index.js

The application will run, start the server, and wait at port 4000 for incoming HTTP requests. Point your browser to http://localhost:4000/hello and confirm the server responds with Hello World! Stop the server by pressing Ctrl+C. The string http://localhost:4000/hello is referred to as a URL (Uniform Resource Locator) and is used to locate a resource on the server. Local labs stay on 4000; a host can later set PORT so the same listen call works remotely.

5.1.6 Configuring Nodemon to Automatically Restart Node.js Server

Slides

React Web applications automatically transpile and restart every time code changes. Node.js can be configured to behave the same way by installing a tool called nodemon which monitors file changes and automatically restarts the Node application. Install nodemon as a development dependency of the server project — that way colleagues and Render get the same script without a global install:

npm install nodemon --save-dev
npx nodemon index.js
# later: npm run dev   (same command, after the script in 5.1.7)

You can also install nodemon globally with npm install nodemon -g (and sudo on macOS). Either works; this book prefers the project-local install so package.json records the tool. Now instead of using the node command to start the server, use nodemon. Confirm the server is still responding Hello World!. Change the response string to Life is good! and without stopping and restarting the server, refresh the browser and confirm that the server now responds with the new string. Keep this process running in the Node folder while next dev runs in the Next.js folder.

To practice, create another endpoint mapped to the root of the application, e.g., /. Navigate to http://localhost:4000 with your browser and confirm the server responds with Welcome to Full Stack Development!

indexwebdev-server/index.js
import express from "express";
const app = express();
app.get("/hello", (req, res) => {
  res.send("Life is good!");
});
app.get("/", (req, res) => {
  res.send("Welcome to Full Stack Development!");
});
app.listen(4000);

5.1.7 Configuring Node.js to Use ES6

The React Web application created in earlier chapters has used the import keyword to load ES6 modules, but older Node samples used the require keyword instead to accomplish the same thing. Since Node version 12, ES6 syntax is supported by configuring the package.json file and adding a new "type" property with value "module" as shown below. Add a start script so npm start runs the server the same way Render will, and a dev script so nodemon is one command:

packagewebdev-server/package.json
{
  "type": "module",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  }
}

Now, instead of using require() to load libraries, the familiar import statement can be used instead. Here is index.js refactored to use import instead of require. Restart the server, refresh the browser, and confirm that the server responds as expected. From here on every server file in this chapter is an ES module, including the .js extension on relative imports — Node requires that extension when "type": "module" is set.

5.1.8 Creating HTTP Routes

The index.js file creates and configures an HTTP server listening for incoming HTTP requests. So far we have created a simple hello HTTP route that responds with a simple string. Throughout this and later chapters, we are going to create quite a few other HTTP routes, too many to define them all in index.js. Instead, it is best practice to group routes into dedicated routing files that collect related HTTP routes. To illustrate this principle, move the routes defined in index.js to the Hello.js file created earlier.

In our case Hello.js handles HTTP requests for a hello greeting and responds with a friendly reply. We are not done though. Notice that Hello.js would reference app which is undefined in that file if we merely copy the two app.get calls. The app variable represents an express instance which we would be tempted to create in the file, but instead only one instance should be shared across all routes implemented per server instance. Instead of creating a new express instance, pass app as a parameter in a function we can import and invoke from index.js as shown below.

Hellowebdev-server/Hello.js
export default function Hello(app) {
  const sayHello = (req, res) => {
    res.send("Life is good!");
  };
  const sayWelcome = (req, res) => {
    res.send("Welcome to Full Stack Development!");
  };
  app.get("/hello", sayHello);
  app.get("/", sayWelcome);
}

Although embedding the callback function declaration within the route definition is perfectly fine, that syntax can be challenging for some. An alternative — arguably better — syntax is to declare the callback functions on their own and then reference them in the route declarations, which is what the listing above does. Import Hello.js and pass app to the function as shown below. Note the .js extension on the import statement. Test http://localhost:4000/hello from the browser and confirm the reply is still friendly.

indexwebdev-server/index.js
import express from "express";
import Hello from "./Hello.js";
const app = express();
Hello(app);
app.listen(4000);

Confirm /hello still replies Life is good! and the root still welcomes you to full stack development. §5.2 adds Lab5/index.js the same way: export a function that receives app, register it from index.js, and keep index.js as the one place that creates the server.

5.2 Lab Exercises

Slides

The following are a set of exercises to practice creating and integrating with an HTTP server from a React Web application. You need two terminals: one for the Next.js user interface on port 3000 and one for the sibling Express server on port 4000. Leave both running for the rest of the chapter. Express LiveDemos below call the companion through httpServer(). §5.3 Route Handler demos stay on same-origin /api/... and do not need port 4000.

# terminal 1 — Next.js UI (port 3000), from webdev-client
npm run dev

# terminal 2 — sibling Express (port 4000), from webdev-server
cd webdev-server
npm run dev          # nodemon index.js
# or: npm start      # node index.js

In your server application, create and import file Lab5/index.js where we will be implementing several server-side exercises. Create a route that welcomes users to Lab 5. Export a default function that accepts the shared app reference — the same pattern as Hello.js in §5.1.8.

Lab5webdev-server/Lab5/index.js
export default function Lab5(app) {
  app.get("/lab5/welcome", (req, res) => {
    res.send("Welcome to Lab 5");
  });
}

Import Lab5/index.js into the server index.js and pass it a reference to express as shown below. Restart the server and confirm that http://localhost:4000/lab5/welcome responds with the expected greeting.

indexwebdev-server/index.js
import express from "express";
import Hello from "./Hello.js";
import Lab5 from "./Lab5/index.js";
const app = express();
Lab5(app);
Hello(app);
app.listen(4000);

In the React Web app project, create a Lab 5 React component to test the Node HTTP server. Import the new component into the existing set of labs and add a new link in the Labs TOC so you can navigate to Lab 5 by selecting the corresponding tab. The example below creates a hyperlink that navigates to the http://localhost:4000/lab5/welcome URL. Confirm the link navigates to the expected response. Do not hard-code the host for long — the next subsection replaces it with an environment variable. Style the link with Tailwind — a simple underline or list item — rather than Bootstrap list-group.

Lab5 pageapp/labs/lab5/page.tsx
export default function Lab5() {
  return (
    <div id="wd-lab5">
      <h2>Lab 5</h2>
      <a id="wd-welcome-link" href="http://localhost:4000/lab5/welcome">
        Welcome
      </a>
    </div>
  );
}

5.2.1 Environment Variables

Slides

Currently we are integrating the React user interface with a Node server, both running locally on our development computers, but ultimately these will both be running on remote servers. Let us configure the local environment so that it will be easy later on to configure our source code to run in any environment. There are two environments to consider: the local development environment and the remote production environment. The local development environment consists of our computer where we do our development running two Node servers, one hosting the React user interface Web application, and the other hosting the Express HTTP server. The remote production environments will consist of the React user interface running on Vercel, and the Express HTTP server running on Render.com, Heroku, or AWS (your choice).

Environments can be configured with environment variables declared in your operating system or as environment files in your project. Environment files are named .env (with a leading period) and can be defined for each environment by appending .development for the local development environment, .test for the test environment, and .production for the production environment. Instead of hardcoding http://localhost:4000 everywhere in our source code, instead, declare an environment variable in the local environment file as shown below. Create the .env.development file at the root of your React project and copy the following content.

NEXT_PUBLIC_HTTP_SERVER=http://localhost:4000

In Next.js React projects, all environment variables that must reach the browser start with NEXT_PUBLIC_ so that they are exported to client components. Each line declares a variable, followed by an equal sign, followed by the value of the variable. Make sure not to use extra spaces or unnecessary extra characters such as quotes, commas, or colons. Every time a new variable is added, removed, or changed in an environment file, the React user interface Web application needs to be restarted. Environment variables can be accessed from the React source code through the global process object, in its env property. For instance, to access the value of NEXT_PUBLIC_HTTP_SERVER declared above, use process.env.NEXT_PUBLIC_HTTP_SERVER.

The PDF name is NEXT_PUBLIC_HTTP_SERVER. This book wraps it in httpServer() so every Express LiveDemo uses the same client code — locally http://localhost:4000, and later whatever origin you set for deploy. Unset, the helper still points at the companion on 4000, which is enough for every LiveDemo in this chapter. §5.5 is when you point the same helper at a deployed origin. Do not hard-code the host in screens.

httpServerapp/lib/httpServer.ts
export function httpServer(): string {
  const raw =
    process.env.NEXT_PUBLIC_HTTP_SERVER ??
    "http://localhost:4000";
  return raw.replace(/\/$/, "");
}

To practice declaring and using environment variables, create the Environment component below and import it from the Lab 5 page. Confirm the browser displays the remote server URL as shown. Make sure the URL to the remote server is never used as a literal in the React source code; instead prefer the environment variable. Replace the http://localhost:4000 in the previous Welcome hyperlink with the helper (or a constant assigned from process.env.NEXT_PUBLIC_HTTP_SERVER). Confirm that the Welcome hyperlink still works.

Environmentapp/labs/lab5/intermediates/5-2-1-Environment.tsx
import { httpServer } from "@/app/lib/httpServer";
export default function Environment() {
  const HTTP_SERVER = httpServer();
  return (
    <div id="wd-lab5-environment">
      <h4>Environment</h4>
      <p>
        <code>NEXT_PUBLIC_HTTP_SERVER</code> = <code>{HTTP_SERVER}</code>
      </p>
      <a id="wd-welcome-link" className="text-blue-700 underline"
        href={`${HTTP_SERVER}/lab5/welcome`}>
        Welcome
      </a>
    </div>
  );
}

Similarly, the Node Express server needs to be configured to run locally on your computer as well as when it is deployed in the remote environment. Refactor index.js so that it uses the remote PORT environment variable if available, or port 4000 when running locally: app.listen(process.env.PORT || 4000).

Environmentapp/labs/lab5/intermediates/5-2-1-Environment.tsx

Environment

NEXT_PUBLIC_HTTP_SERVER = http://localhost:4000

Welcome

5.2.2 Sending Data to a Server via HTTP Requests

Slides

Let us explore how we can integrate the React user interface with the Node server by sending information to the server from the browser. There are three ways to send information to the server:

  1. Path parameters — parameters are encoded as segments of the path itself, e.g., /lab5/add/2/5.
  2. Query parameters — parameters are encoded as name value pairs in the query string after the ? character at the end of a URL, e.g., /lab5/add?a=2&b=5.
  3. Request body — data is sent as a string representation of data encoded in some format such as XML or JSON containing properties and their values, e.g., {a: 2, b: 5}.

We will explore the first two in this section, and address the last one towards the end of the labs in §5.2.6.

5.2.2.1 Sending Data to a Server with Path Parameters

React applications can pass data to servers by embedding it in a URL path as path parameters part of a URL. For instance the last two integers — 2 and 4 — at the end of the following URL can be parsed by a corresponding matching route on the server, add the two integers, and respond with the result of 6: /lab5/add/2/4. The following route declarations can parse path parameters a and b encoded in paths /lab5/add/:a/:b and /lab5/subtract/:a/:b. In PathParameters.js, implement the routes below and import it in Lab5/index.js. On your own create routes /lab5/multiply/:a/:b and /lab5/divide/:a/:b that calculate the arithmetic multiplication and division.

Retrieve path parameters as strings from req.params, parse them as integers, then send the result as a string. Do not send a bare integer: browsers and some HTTP clients treat a numeric body as a status code, so res.send(6) can look like a 6 status instead of the text six. Convert with .toString().

PathParameterswebdev-server/Lab5/PathParameters.js
export default function PathParameters(app) {
  const add = (req, res) => {
    const { a, b } = req.params;
    const sum = parseInt(a) + parseInt(b);
    res.send(sum.toString());
  };
  const subtract = (req, res) => {
    const { a, b } = req.params;
    res.send((parseInt(a) - parseInt(b)).toString());
  };
  app.get("/lab5/add/:a/:b", add);
  app.get("/lab5/subtract/:a/:b", subtract);
}

Note when you import, make sure to include the extension .js. Pass a reference of app to the PathParameters function. Confirm that http://localhost:4000/lab5/add/6/4 responds with 10 and http://localhost:4000/lab5/subtract/6/4 responds with 2. Also confirm the routes you implemented on your own.

Lab5webdev-server/Lab5/index.js
import PathParameters from "./PathParameters.js";
export default function Lab5(app) {
  app.get("/lab5/welcome", (req, res) => {
    res.send("Welcome to Lab 5");
  });
  PathParameters(app);
}

Meanwhile, in the React Web application, let us create a React component to test the new routes from our Web application. Web applications that interact with server applications are often referred to as client applications since they are the client in an application built using a client/server architecture. Create the component below that declares state variables a and b, encodes the values in hyperlinks, and when you click them, the server responds with the addition or subtraction of the parameters. Note that the name of the component is arbitrary. The fact that it is called the same as the routes in the server is a coincidence. It also helps us keep track of which UI components on the client are related to the server resources. On your own, create links that invoke the multiply and divide routes you implemented earlier. Import the new component in your Lab 5 component and confirm that clicking the links generates the expected response. Use Tailwind inputs and colored buttons — the PDF used Bootstrap FormControl and btn-primary; the live sample uses rounded borders and bg-blue-600.

PathParametersapp/labs/lab5/intermediates/5-2-2-1-PathParameters.tsx
const HTTP_SERVER = httpServer();
const [a, setA] = useState("34");
const [b, setB] = useState("23");
<a id="wd-path-parameter-add"
  href={`${HTTP_SERVER}/lab5/add/${a}/${b}`}>
  Add {a} + {b}
</a>
PathParametersapp/labs/lab5/intermediates/5-2-2-1-PathParameters.tsx

5.2.2.2 Sending Data to a Server with Query Parameters

React applications can also send data to servers by encoding it as a query string after the question mark character (?) at the end of a URL. A query string consists of a list of name value pairs separated by the ampersand character (&) as shown below: /lab5/calculator?operation=add&a=2&b=4. In Lab5/QueryParameters.js, in your server application, create a route that can parse an operation and its parameters a and b. If the operation is add the route responds with the addition of the parameters. If the operation is subtract, the route responds with the subtraction of the parameters. On your own, also handle operations multiply and divide. Import QueryParameters.js in Lab5/index.js and pass it a reference of app. Read the values from req.query — they arrive as strings, the same as path parameters — and send the result as a string so the browser does not treat it as a status code.

QueryParameterswebdev-server/Lab5/QueryParameters.js
export default function QueryParameters(app) {
  const calculator = (req, res) => {
    const { a, b, operation } = req.query;
    let result = 0;
    switch (operation) {
      case "add":
        result = parseInt(a) + parseInt(b);
        break;
      case "subtract":
        result = parseInt(a) - parseInt(b);
        break;
      default:
        result = "Invalid operation";
    }
    res.send(result.toString());
  };
  app.get("/lab5/calculator", calculator);
}

In a new component QueryParameters.tsx, in your React Web application, create hyperlinks to test the new route. You will need a constant initialized to the environment variable through httpServer(). Confirm the following hyperlinks work as expected: add with 34 and 23 should respond 57; subtract should respond 11. Create additional links to test multiply and divide, using IDs starting with wd-query-parameter-.

QueryParametersapp/labs/lab5/intermediates/5-2-2-2-QueryParameters.tsx
<a id="wd-query-parameter-add"
  href={`${HTTP_SERVER}/lab5/calculator?operation=add&a=${a}&b=${b}`}>
  Add {a} + {b}
</a>
QueryParametersapp/labs/lab5/intermediates/5-2-2-2-QueryParameters.tsx

5.2.2.3 On Your Own

On your own, remember to implement multiply and divide requests on the client and server that demonstrate multiplying and dividing numbers encoded in the request's path. Now implement the same operations again, multiply and divide on the server and client, but multiplying and dividing parameters encoded in the query string. Both pairs of links should appear in the LiveDemos above once you finish — path IDs start with wd-path-parameter-, query IDs with wd-query-parameter-.

5.2.3 Working with Remote Objects on a Server

Slides

The examples so far have demonstrated working with integers and strings, but all primitive datatypes work as well, including objects and arrays. The example below declares an assignment object accessible at the route /lab5/assignment. Import it into your Lab5/index.js in your server. The object state persists as long as the server is running; changes to the object persist until you reboot, which resets the object to the seed values. Use res.json instead of res.send when you know the response is formatted as JSON so Express sets the content type and serializes the object for you.

WorkingWithObjectswebdev-server/Lab5/WorkingWithObjects.js
const assignment = {
  id: 1,
  title: "NodeJS Assignment",
  description: "Create a NodeJS server with ExpressJS",
  due: "2021-10-10",
  completed: false,
  score: 0,
};
export default function WorkingWithObjects(app) {
  const getAssignment = (req, res) => {
    res.json(assignment);
  };
  app.get("/lab5/assignment", getAssignment);
}

5.2.3.1 Retrieving Objects from a Server

In your React project, create a WorkingWithObjects component to test the new route as shown below. Import it in Lab 5 and confirm that http://localhost:4000/lab5/assignment responds with the assignment object. The Get Assignment hyperlink navigates the browser to that URL so you can see the raw JSON — later sections will fetch the same object without leaving the page.

Retrieving Objectsapp/labs/lab5/intermediates/5-2-3-WorkingWithObjects.tsx
<h4>Retrieving Objects</h4>
<a id="wd-retrieve-assignments"
  className="rounded bg-blue-600 px-3 py-1.5 text-sm text-white"
  href={`${HTTP_SERVER}/lab5/assignment`}>
  Get Assignment
</a>

5.2.3.2 Retrieving Object Properties from a Server

We can retrieve individual properties in an object such as the title shown below. Add a getAssignmentTitle handler that responds with only assignment.title, mapped to /lab5/assignment/title. Confirm that http://localhost:4000/lab5/assignment/title retrieves the assignment's title. In WorkingWithObjects, add a link that retrieves the title as shown below. Confirm that clicking the link retrieves the assignment's title as a JSON string.

getAssignmentTitlewebdev-server/Lab5/WorkingWithObjects.js
const getAssignmentTitle = (req, res) => {
  res.json(assignment.title);
};
app.get("/lab5/assignment/title", getAssignmentTitle);
app.get("/lab5/assignment", getAssignment);
Retrieving Propertiesapp/labs/lab5/intermediates/5-2-3-WorkingWithObjects.tsx
<h4>Retrieving Properties</h4>
<a id="wd-retrieve-assignment-title"
  href={`${HTTP_SERVER}/lab5/assignment/title`}>
  Get Title
</a>

5.2.3.3 Modifying Objects in a Server

We can also use routes to modify objects or individual properties as shown below. The route retrieves the new title from the path and updates the assignment object's title property. Changes to objects in the server persist as long as the server is running; rebooting the server resets the object state to the constants at the top of the file.

setAssignmentTitlewebdev-server/Lab5/WorkingWithObjects.js
const setAssignmentTitle = (req, res) => {
  const { newTitle } = req.params;
  assignment.title = newTitle;
  res.json(assignment);
};
app.get("/lab5/assignment/title/:newTitle", setAssignmentTitle);

In the WorkingWithObjects component in your React project, create an assignment state variable to test editing the assignment object on the server. Create an input field where we can type the new assignment title, and a link that invokes the route that updates the title. Eventually we will fetch this initial data from the server and populate the form with the remote data so we can modify it here in the UI; for now the seed matches the server object so the first click is a no-op until you type. Confirm that you can change the assignment's title: type a new string, click Update Title, then click Get Assignment and see the new title in the JSON.

Modifying Propertiesapp/labs/lab5/intermediates/5-2-3-WorkingWithObjects.tsx
const [assignment, setAssignment] = useState({
  id: 1, title: "NodeJS Assignment",
  description: "Create a NodeJS server with ExpressJS",
  due: "2021-10-10", completed: false, score: 0,
});
const ASSIGNMENT_API_URL = `${HTTP_SERVER}/lab5/assignment`;
<a id="wd-update-assignment-title"
  href={`${ASSIGNMENT_API_URL}/title/${assignment.title}`}>
  Update Title
</a>
<input id="wd-assignment-title" value={assignment.title}
  onChange={(e) => setAssignment({ ...assignment, title: e.target.value })} />

5.2.3.4 On Your Own

Now, on your own, create a module object with string properties id, name, description, and course. Feel free to use values of your choice. Create a route that responds with the module object, mapped to /lab5/module. In the UI, create a link labeled Get Module that retrieves the module object from the server. Confirm that clicking the link retrieves the module. Create another route mapped to /lab5/module/name that retrieves the name of the module created earlier. In the UI, create a hyperlink labeled Get Module Name that retrieves the name of the module object. Confirm that clicking the link retrieves the module's name.

On your own, in WorkingWithObjects.tsx, create a module state variable to test editing the module object on the server. Create an input field where we can type the new module name, and a link that invokes the route that updates the name. Confirm that you can change the module's name. Create routes and a corresponding UI that can modify the score and completed properties of the assignment object. In the React application, create an input field of type number where you can type the new score and an input field of type checkbox where you can select the completed property. Create a link that updates the score and another link that updates the completed property. For the module, create routes and UI to edit the module's description.

WorkingWithObjectsapp/labs/lab5/intermediates/5-2-3-WorkingWithObjects.tsx

Working With Objects

Retrieving Objects

Get Assignment

Retrieving Properties

Get Title

Modifying Properties

Update Title

Module (On Your Own)

Get ModuleGet Module Name

5.2.4 Working with Remote Arrays on a Server

Slides

Now let us work with something a little more challenging. Create an array of objects and explore how to retrieve, add, remove, and update the array. Working with a collection of objects requires a general set of operations often referred to as CRUD or create, read, update, and delete. These operations capture common interactions with any collection of data such as creating and adding new instances to the collection, reading or retrieving items in a collection, updating or modifying items in a collection, and deleting or removing items from a collection. Same process memory as the assignment object — reboot resets the seed.

5.2.4.1 Retrieving Arrays from a Server

Let us first create the array data structure containing several todo objects. The most common integration between a client and server application is for a client application to retrieve all the instances of some collection. To illustrate this, create a route that retrieves the array of todo objects as shown below. Import the new route to Lab5/index.js. Point the browser to http://localhost:4000/lab5/todos and confirm the server responds with the array of todos.

WorkingWithArrayswebdev-server/Lab5/WorkingWithArrays.js
let todos = [
  { id: 1, title: "Task 1", completed: false },
  { id: 2, title: "Task 2", completed: true },
  { id: 3, title: "Task 3", completed: false },
  { id: 4, title: "Task 4", completed: true },
];
export default function WorkingWithArrays(app) {
  const getTodos = (req, res) => {
    res.json(todos);
  };
  app.get("/lab5/todos", getTodos);
}

In a new React component, create a hyperlink to test retrieving all todo objects in the array from the client. Add the new component to the Lab 5 component and confirm that clicking the link retrieves the array.

Retrieving Arraysapp/labs/lab5/intermediates/5-2-4-WorkingWithArrays.tsx
const API = `${HTTP_SERVER}/lab5/todos`;
<a id="wd-retrieve-todos" href={API}>Get Todos</a>

5.2.4.2 Retrieving Data From a Server by Primary Key

Another common operation is to retrieve a particular item from an array by its primary key, e.g., its ID property. The convention is to encode the ID of the item of interest as a path parameter. Note that we could have chosen to encode the ID as a query parameter instead, but it is best practice to encode identifiers in the path instead. The example below parses the ID as a path parameter, finds the corresponding item, and responds with the item. Add a hyperlink to the React component to test retrieving an item from the array by its primary key. Confirm that you can type the ID in the UI and clicking the hyperlink retrieves the corresponding item.

getTodoByIdwebdev-server/Lab5/WorkingWithArrays.js
const getTodoById = (req, res) => {
  const { id } = req.params;
  const todo = todos.find((t) => t.id === parseInt(id));
  res.json(todo);
};
app.get("/lab5/todos", getTodos);
app.get("/lab5/todos/:id", getTodoById);
Get Todo by IDapp/labs/lab5/intermediates/5-2-4-WorkingWithArrays.tsx
const [todo, setTodo] = useState({ id: "1" });
<a id="wd-retrieve-todo-by-id" href={`${API}/${todo.id}`}>
  Get Todo by ID
</a>
<input id="wd-todo-id" value={todo.id}
  onChange={(e) => setTodo({ ...todo, id: e.target.value })} />

5.2.4.3 Filtering Data From a Server With a Query String

The convention for retrieving a particular item from a collection is to encode the item's ID as a path parameter, e.g., /todos/123. Another convention is that if the primary key is not provided, then the interpretation is that we want the entire collection of items, e.g., /todos. We can also want to retrieve items by some other criteria other than the item's ID such as the item's title or completed properties. The best practice in this case is to use query strings instead of path parameters when filtering items by properties other than the primary key, e.g., /todos?completed=true. The example below refactors /lab5/todos to handle the case when we want to filter the array by the completed query parameter. Add a hyperlink to the React component to test retrieving all completed todos.

filter todoswebdev-server/Lab5/WorkingWithArrays.js
const getTodos = (req, res) => {
  const { completed } = req.query;
  if (completed !== undefined) {
    const completedBool = completed === "true";
    res.json(todos.filter((t) => t.completed === completedBool));
    return;
  }
  res.json(todos);
};
Filtering Array Itemsapp/labs/lab5/intermediates/5-2-4-WorkingWithArrays.tsx
<a id="wd-retrieve-completed-todos" href={`${API}?completed=true`}>
  Get Completed Todos
</a>

5.2.4.4 Creating New Data in a Server

The examples we have seen so far have illustrated various read operations in our exploration of possible CRUD operations. Let us now take a look at the create operation. The example below demonstrates a route that creates a new item in the array and responds with the array now containing the new item. Note that it is implemented before the /lab5/todos/:id route, otherwise the :id path parameter would interpret the word create in /lab5/todos/create as an ID, which would certainly create an error trying to parse it as an integer. Also note that the new todo creates default values including a unique identifier field id based on a timestamp. Eventually primary keys will be handled by a database later in the course. Finally note that the response consists of the entire todos array, which is convenient for us for now, but a more common implementation would be to respond with only newTodo.

createNewTodowebdev-server/Lab5/WorkingWithArrays.js
const createNewTodo = (req, res) => {
  const newTodo = {
    id: new Date().getTime(),
    title: "New Task",
    completed: false,
  };
  todos.push(newTodo);
  res.json(todos);
};
app.get("/lab5/todos/create", createNewTodo);
app.get("/lab5/todos/:id", getTodoById);

Add a Create Todo hyperlink to the WorkingWithArrays component to test the new route. Confirm that clicking the link creates the new item in the array — Get Todos afterwards should include New Task.

Creating new Itemsapp/labs/lab5/intermediates/5-2-4-WorkingWithArrays.tsx
<a id="wd-create-todo" href={`${API}/create`}>Create Todo</a>

5.2.4.5 Deleting Data from a Server

Next let us consider the delete operation in the CRUD family of operations. The convention is to encode the ID of the item to delete as a path parameter as shown below. We search for the item in the set of items and remove it. Typically we would respond with a status of success or failure, but for now we are responding with all the todos. To test the new route, create a link that encodes the todo's ID in a hyperlink to delete the corresponding item. We will use the todo state variable created earlier to type the ID of the item we want to remove. Confirm that you can type the ID of an item, click the hyperlink, and that the corresponding item is removed from the array.

removeTodowebdev-server/Lab5/WorkingWithArrays.js
const removeTodo = (req, res) => {
  const { id } = req.params;
  const todoIndex = todos.findIndex((t) => t.id === parseInt(id));
  todos.splice(todoIndex, 1);
  res.json(todos);
};
app.get("/lab5/todos/:id/delete", removeTodo);
Removing from an Arrayapp/labs/lab5/intermediates/5-2-4-WorkingWithArrays.tsx
<a id="wd-remove-todo" href={`${API}/${todo.id}/delete`}>
  Remove Todo with ID = {todo.id}
</a>

5.2.4.6 Updating Data on a Server

Finally let us consider the update operation in the CRUD family of operations. The convention is to encode the ID of the item to update as a path parameter as shown below. We search for the item in the set of items and update it. Typically we would respond with a status of success or failure, but for now we are responding with all the todos. Group the callback functions together towards the top of the routing file and the route declarations grouped towards the bottom of the file. To test the new route, add an input field to edit the title property and a link that encodes both the ID of the item and the new value of the title property as shown below.

updateTodoTitlewebdev-server/Lab5/WorkingWithArrays.js
const updateTodoTitle = (req, res) => {
  const { id, title } = req.params;
  const todo = todos.find((t) => t.id === parseInt(id));
  todo.title = title;
  res.json(todos);
};
app.get("/lab5/todos/:id/title/:title", updateTodoTitle);
Updating an Itemapp/labs/lab5/intermediates/5-2-4-WorkingWithArrays.tsx
<a href={`${API}/${todo.id}/title/${todo.title}`} id="wd-update-todo-title">
  Update Todo
</a>
<input value={todo.id} onChange={(e) => setTodo({ ...todo, id: e.target.value })} />
<input value={todo.title} onChange={(e) => setTodo({ ...todo, title: e.target.value })} />

5.2.4.7 On Your Own

Using the exercises so far as examples, implement routes and corresponding UI that allows editing completed and description properties of todo items identified by their ID. Create the routes below in the Node.js HTTP server project. In the WorkingWithArrays component, add a text input field to edit the description and a checkbox input field to edit the completed property. Create a link that updates the description of the todo item whose id is encoded in the URL and another link that updates the completed property of the todo item whose id is encoded in the URL.

  • completed — /lab5/todos/:id/completed/:completed — responds with todos — test link Complete Todo ID = 1
  • description — /lab5/todos/:id/description/:description — responds with todos — test link Describe Todo ID = 1
WorkingWithArraysapp/labs/lab5/intermediates/5-2-4-WorkingWithArrays.tsx

Working with Arrays

Retrieving Arrays

Get Todos

Retrieving an Item from an Array by ID

Get Todo by ID

Filtering Array Items

Get Completed Todos

Creating new Items in an Array

Create Todo

Removing from an Array

Remove Todo with ID = 1

Updating an Item in an Array

Update Todo

5.2.5 Asynchronous Communication with HTTP Servers

Slides

The exercises explored so far sent data encoded in the URL of hyperlinks. The links navigated to a separate browser window that displayed the server response. Even though we were able to send data to the server and affect changes to the server data, we have not considered how those changes can affect the user interface. Let us explore how to fully integrate the user interface with the server by sending and receiving HTTP requests and responses asynchronously.

5.2.5.1 Asynchronous JavaScript and XML

JavaScript Web applications, such as React applications, can communicate with server applications using a technology called AJAX or Asynchronous JavaScript and XML. Using AJAX, JavaScript applications can send and retrieve HTTP requests and responses asynchronously from client JavaScript applications to a remote HTTP server. Although XML was the original data format in AJAX, JSON has overtaken as the dominant data format in modern Web applications, but the AJAX label still applies nevertheless. Axios is a popular JavaScript library that React user interface applications can use to communicate with servers using AJAX. Install the library at the root of the React Web application project as shown below.

npm install axios

Let us use the same server routes implemented in earlier exercises, but instead of clicking on hyperlinks in the React client, we will use axios to programmatically invoke the URLs, giving us a chance to capture and handle the responses from the server and render the response in the user interface. The code below illustrates how to use the axios library to send an asynchronous request to the server and then capture the response in the user interface, without navigating to the URL, away from the current window. The fetchWelcomeOnClick function is tagged as async since it uses axios.get() to asynchronously send a request to the server, and returns the response from the server. Create the component below and import it in the Lab 5 component. Open the Web Dev Tools and confirm that clicking the Fetch Welcome button causes a CORS error the first time — that is expected until the next subsection.

HttpClientapp/labs/lab5/intermediates/5-2-5-HttpClient.tsx
import axios from "axios";
const HTTP_SERVER = httpServer();
const [welcomeOnClick, setWelcomeOnClick] = useState("");
const fetchWelcomeOnClick = async () => {
  const response = await axios.get(`${HTTP_SERVER}/lab5/welcome`);
  setWelcomeOnClick(response.data);
};
<button type="button" onClick={fetchWelcomeOnClick}>Fetch Welcome</button>
<p>Response from server: <b>{welcomeOnClick}</b></p>

5.2.5.2 Configuring Cross Origin Request Sharing (CORS)

Servers and browsers limit JavaScript programs to only be able to communicate with the servers from where they are downloaded. Since our React application is running locally from localhost:3000, then they would only be able to communicate back to a server running on localhost:3000, but our server is running on localhost:4000, so when our JavaScript components try to communicate with localhost:4000, the browser considers a different origin as a security risk, stops the communication, and throws a CORS exception. This course's Next.js UI is port 3000. The mismatch with Express on 4000 is the same problem any two-origin setup has.

CORS stands for Cross-Origin Resource Sharing, which governs the policies and mechanisms of how various resources can be shared across different domains or origins. Browsers enforce CORS policies by first checking with the server if they are okay with receiving requests from different domains. If the server responds affirmatively, then browsers let the requests go through, otherwise they will consider the attempt as a violation of CORS security policy, abort the request, and throw the exception. We can configure the CORS security policies by installing the cors Node.js library as shown below.

npm install cors

In index.js, import the cors library and configure it as shown below to allow all requests from any origin. We will narrow down this policy in a later section when sessions and cookies arrive. Make sure cors is used right after creating the app express instance and before the routes. Restart the server and refresh the React application. Confirm that the user interface is able to retrieve the Welcome to Lab 5 message from the server without errors.

corswebdev-server/index.js
import cors from "cors";
const app = express();
app.use(cors());
Lab5(app);

5.2.5.3 Creating a Client Library

The current HttpClient implementation makes a request to the server from the component itself using the axios library. In addition to retrieving (or reading) data from the server, there will be other CRUD operations to create, update, and delete needed to interact with the server. Instead of implementing these in a React component, it is better to implement these in a reusable client library that can be shared across several user interface components. Move the axios.get() in HttpClient to a separate file called app/labs/lab5/client.ts as shown below.

clientapp/labs/lab5/client.ts
import axios from "axios";
import { httpServer } from "@/app/lib/httpServer";
const HTTP_SERVER = httpServer();
export const fetchWelcomeMessage = async () => {
  const response = await axios.get(`${HTTP_SERVER}/lab5/welcome`);
  return response.data;
};

Now refactor HttpClient to use the client as shown below. Confirm that clicking on Fetch Welcome still works. Screens share one library so when the welcome URL or the helper changes, you edit one file.

HttpClient uses clientapp/labs/lab5/intermediates/5-2-5-HttpClient.tsx
import * as client from "../client";
const fetchWelcomeOnClick = async () => {
  const message = await client.fetchWelcomeMessage();
  setWelcomeOnClick(message);
};

5.2.5.4 Retrieving Data from a Server on Component Load

The previous exercise fetched data from the server when the user requested it. Often times we need to retrieve data from the server when you first navigate to a screen or a component is first loaded and displayed. Use React's useEffect hook function as shown below to invoke fetchWelcomeOnLoad when a component or screen first loads. Now, when the HttpClient loads, the useEffect invokes fetchWelcomeOnLoad which retrieves the message from the server and sets the new welcomeOnLoad state variable. Confirm that if you refresh the screen, the welcome message appears without having to click on the Fetch Welcome button.

useEffect loadapp/labs/lab5/intermediates/5-2-5-HttpClient.tsx
const [welcomeOnLoad, setWelcomeOnLoad] = useState("");
const fetchWelcomeOnLoad = async () => {
  const welcome = await client.fetchWelcomeMessage();
  setWelcomeOnLoad(welcome);
};
useEffect(() => {
  fetchWelcomeOnLoad();
}, []);
HttpClientapp/labs/lab5/intermediates/5-2-5-HttpClient.tsx

HTTP Client

Requesting on Click

Response from server:

Requesting on Load

Response from server:


5.2.5.5 Working with Remote Objects on a Server Asynchronously

Let us now revisit the APIs that worked with the assignment object in WorkingWithObjects and create an asynchronous version. Let us add client functions to client.ts to fetch the assignment object from the server and update its title as shown below.

assignment clientapp/labs/lab5/client.ts
const ASSIGNMENT_API = `${HTTP_SERVER}/lab5/assignment`;
export const fetchAssignment = async () => {
  const response = await axios.get(ASSIGNMENT_API);
  return response.data;
};
export const updateTitle = async (title: string) => {
  const response = await axios.get(`${ASSIGNMENT_API}/title/${title}`);
  return response.data;
};

Then, in a new WorkingWithObjectsAsynchronously component, create a UI that fetches the assignment on load and then allows you to edit its title. Import the component in Lab 5 and confirm that the assignment is displayed on load. The form fields bind to local state so you can type; the pre shows the JSON snapshot so you can see every property the server sent. Now add a button to update the assignment's title. Change the assignment's title, click Update Title, refresh the screen, and confirm that the title has changed — the new title is still on the server.

WorkingWithObjectsAsynchronouslyapp/labs/lab5/intermediates/5-2-5-WorkingWithObjectsAsync.tsx
const [assignment, setAssignment] = useState({});
const fetchAssignment = async () => {
  setAssignment(await client.fetchAssignment());
};
const updateTitle = async () => {
  setAssignment(await client.updateTitle(assignment.title));
};
useEffect(() => { fetchAssignment(); }, []);
WorkingWithObjectsAsynchronouslyapp/labs/lab5/intermediates/5-2-5-WorkingWithObjectsAsync.tsx

Working with Objects Asynchronously

Assignment

{}

5.2.5.6 Working with Remote Arrays on a Server Asynchronously

Now let us do the same thing to the arrays. Let us use axios so that we can manipulate remote arrays on the server from the user interface and update a user interface to reflect the changes in the remote array. We will implement several client functions in client.ts that use axios to communicate with the server and we will use them from a new component that will render the remote array in the user interface. The exercise below fetches the todos from the server and populates a todos state variable which we can then render as a list of todos when the component loads. Confirm that the todos render when the component first loads. Strike through completed titles so the list reads like a checklist. The finished LiveDemo for this component waits until §5.2.6.4, after POST, DELETE, PUT, and error handling are on the same screen.

fetchTodosapp/labs/lab5/client.ts
const TODOS_API = `${HTTP_SERVER}/lab5/todos`;
export const fetchTodos = async () => {
  const response = await axios.get(TODOS_API);
  return response.data;
};
WorkingWithArraysAsynchronouslyapp/labs/lab5/intermediates/5-2-6-WorkingWithArraysAsync.tsx
const [todos, setTodos] = useState([]);
const fetchTodos = async () => {
  setTodos(await client.fetchTodos());
};
useEffect(() => { fetchTodos(); }, []);
{todos.map((todo) => (
  <li key={todo.id}>
    <input type="checkbox" defaultChecked={todo.completed} />
    <span className={todo.completed ? "line-through" : ""}>{todo.title}</span>
  </li>
))}

5.2.5.7 Deleting Data from a Server Asynchronously

In client.ts, add a removeTodo client function that sends a delete request to the server. The server will respond with an array with the surviving todos. In the WorkingWithArraysAsynchronously component, add remove buttons to each of the todos that invoke a new removeTodo function that uses the client to send an asynchronous delete request to the server and updates the todos state variable with the surviving todos. Use a trashcan icon to represent the remove button. Confirm that clicking on the new remove buttons actually removes the corresponding todo.

removeTodoapp/labs/lab5/client.ts
export const removeTodo = async (todo: { id: number }) => {
  const response = await axios.get(`${TODOS_API}/${todo.id}/delete`);
  return response.data;
};
remove buttonapp/labs/lab5/intermediates/5-2-6-WorkingWithArraysAsync.tsx
const removeTodo = async (todo) => {
  setTodos(await client.removeTodo(todo));
};
<FaTrash onClick={() => removeTodo(todo)} id="wd-remove-todo"
  className="cursor-pointer text-red-600" />

5.2.5.8 Creating New Data in a Server Asynchronously

A previous exercise implemented a server route to create new todo items. In the React project client.ts implement a createNewTodo client function that requests creating a new todo item from the server as shown below. In the WorkingWithArraysAsynchronously component, add a + button icon to invoke the createNewTodo client function and update the todos state variable with the todos from the server. Confirm that clicking the + button icon actually creates a new todo.

createNewTodoapp/labs/lab5/client.ts
export const createNewTodo = async () => {
  const response = await axios.get(`${TODOS_API}/create`);
  return response.data;
};
create buttonapp/labs/lab5/intermediates/5-2-6-WorkingWithArraysAsync.tsx
const createNewTodo = async () => {
  setTodos(await client.createNewTodo());
};
<FaPlusCircle onClick={createNewTodo} id="wd-create-todo"
  className="cursor-pointer text-green-600" />

5.2.6 Passing JSON Data to a Server in an HTTP Body

The exercises so far have sent data to the server as path and query parameters. This approach is limited to the maximum length of the URL string, and only string data types. Another concern is that data in the URL is sent over a network in clear text, so anyone snooping around between the client and server can see the data as plain text which is not a good option for exchanging sensitive information such as passwords and other personal data. A better approach is to encode the data as JSON in the HTTP request body which allows for arbitrarily large amounts of data as well as secure data encryption. To enable the server to parse JSON data from the request body, add the following app.use() statement in index.js. Make sure that it is implemented right after the CORS configuration statement. Now JSON data coming from the client is available in the request body in the request.body property in the server routes.

express.jsonwebdev-server/index.js
app.use(cors());
app.use(express.json());
Lab5(app);

The hyperlinks and axios.get() in the exercises so far have sent data to the server using the HTTP GET method or verb. HTTP defines several other HTTP methods or verbs including:

  • GET — for retrieving data, but we have been also misusing it for creating, modifying and deleting data on the server. We will start using it properly only for retrieving data.
  • POST — for creating new data typically embedded in the HTTP body
  • PUT — for modifying existing data where updates are typically embedded in the HTTP body
  • DELETE — for removing existing data
  • OPTIONS — for retrieving allowed operations. Used to figure out if CORS policy allows communication with the other methods such as GET, POST, PUT and DELETE

The GET method, as the name suggests, is meant for only getting data from the server. We have been misusing it to implement routes that also create, update, and delete data on the server. We did this mostly for academic purposes since it is the easiest HTTP method to work with. From now on we will use the proper HTTP method for the right purpose. Keep the older GET create/delete routes so earlier links still work.

5.2.6.1 Posting Data to Servers with HTTP POST Requests

To illustrate using the HTTP POST method, let us re-implement the route that creates new todos as shown below. The HTTP POST method takes the role of the verb meaning create. Add the new app.post implementation. Do not remove the old GET version so we do not break the other lab exercises. Note how the new implementation grabs the posted JSON data from req.body and uses it to define newTodo. Also note that this version does not respond with the entire todos array and instead only responds with the newly created todo object instance. This is more reasonable since arrays can potentially be large and it would be expensive to transfer such large data structures over a network, especially if the client UI already has most of this data already displayed.

postNewTodowebdev-server/Lab5/WorkingWithArrays.js
const postNewTodo = (req, res) => {
  const newTodo = { ...req.body, id: new Date().getTime() };
  todos.push(newTodo);
  res.json(newTodo);
};
app.get("/lab5/todos/create", createNewTodo);
app.post("/lab5/todos", postNewTodo);

Back in the user interface, add a new postNewTodo client function in client.ts that posts new todo objects to the server. Note the second argument in the axios.post() method containing the new todo object instance sent to the server. The response this time contains the todo instance added to the todos array in the server instead of all the todos on the server. In the WorkingWithArraysAsynchronously component, create a new + button icon to invoke the new postNewTodo client function to send a new todo object to the server containing a default title and completed properties. Append the new todo object created on the server to the local todos state variable to update the user interface with the new todo. Color the new + button icon a different color so it is distinguishable from the createNewTodo button. Confirm that you can add new items to the array when you click on the new + button icon.

postNewTodo clientapp/labs/lab5/client.ts
export const postNewTodo = async (todo: { title: string; completed: boolean }) => {
  const response = await axios.post(TODOS_API, todo);
  return response.data;
};
post buttonapp/labs/lab5/intermediates/5-2-6-WorkingWithArraysAsync.tsx
const postNewTodo = async () => {
  const newTodo = await client.postNewTodo({
    title: "New Posted Todo",
    completed: false,
  });
  setTodos([...todos, newTodo]);
};
<FaPlusCircle onClick={postNewTodo} id="wd-post-todo"
  className="cursor-pointer text-blue-600" />

5.2.6.2 Deleting Data from Servers with HTTP DELETE Requests

Now that we have axios we can implement a better version of the remove operation. The current removeTodo implementation uses the HTTP GET method to request the server to remove data. The HTTP DELETE method is specifically suited for removing data from remote servers. In the server project, implement a better version of the delete operation as shown below. The new implementation uses the HTTP DELETE method declared in app.delete() which is distinct from app.get() for which we do not need the trailing /delete at the end of the URL. Removing the element from the array is the same either way. Although we could again respond with the entire array of surviving todos, it is better to just respond with a success status and let the user interface update its state variable. This reduces unnecessary data communication between the client and server.

deleteTodowebdev-server/Lab5/WorkingWithArrays.js
const deleteTodo = (req, res) => {
  const { id } = req.params;
  const todoIndex = todos.findIndex((t) => t.id === parseInt(id));
  todos.splice(todoIndex, 1);
  res.sendStatus(200);
};
app.delete("/lab5/todos/:id", deleteTodo);
app.get("/lab5/todos/:id/delete", removeTodo);

In the React project create a new client function called deleteTodo. Note how it is implemented using axios.delete instead of axios.get so that it matches the server's app.delete as well as the URL format without the trailing /delete. In the user interface component, add another delete button to try this new deleteTodo client function, but use a different icon, say an X so as to not confuse it with the trash. Note that the new implementation ignores the response from the server and instead filters the removed todo from the local state variable. This is fine for now, but the operation is too optimistic assuming the server successfully deleted the item from the array and updating the user interface without confirmation. Later we will deal with errors from the server to make sure the local state variable in the user interface is in synch with the remote array on the server.

deleteTodo clientapp/labs/lab5/client.ts
export const deleteTodo = async (todo: { id: number }) => {
  const response = await axios.delete(`${TODOS_API}/${todo.id}`);
  return response.data;
};
X deleteapp/labs/lab5/intermediates/5-2-6-WorkingWithArraysAsync.tsx
const deleteTodo = async (todo) => {
  await client.deleteTodo(todo);
  setTodos(todos.filter((t) => t.id !== todo.id));
};
<TiDelete onClick={() => deleteTodo(todo)} id="wd-delete-todo"
  className="cursor-pointer text-2xl text-red-600" />

5.2.6.3 Updating Data on Servers with HTTP PUT Requests

Use HTTP PUT to reimplement the route that updates an item in an array as shown below. The route replaces the todo item whose ID matches the id path parameter with a combination of the original todo object and properties in the req.body. This overrides any properties in the original todo object with matching properties in the request body. Note that this new implementation does not respond with the todos array, but instead responds with a simple OK status code of 200. This is more reasonable since there is no need to respond with an entire array since the user interface already has the array cached in the browser and it can just update the item in the todos state variable.

updateTodo PUTwebdev-server/Lab5/WorkingWithArrays.js
const updateTodo = (req, res) => {
  const { id } = req.params;
  todos = todos.map((t) => {
    if (t.id === parseInt(id)) {
      return { ...t, ...req.body };
    }
    return t;
  });
  res.sendStatus(200);
};
app.put("/lab5/todos/:id", updateTodo);

Back in the user interface, in client.ts add a new updateTodo function that puts updates to the server. Note the second argument in the axios.put() method containing the updated todo object instance sent to the server. The response contains a status. In the WorkingWithArraysAsynchronously component, add an input field that shows up when you click a new pencil icon by setting the todo's editing property to true. Pressing the Enter key sets the todo's editing property to false, and shows the updated title again. Add an onChange attribute to the completed checkbox so that it updates the corresponding property of the todo object. Confirm you can edit the title and completed properties of the todos, and that the changes persist after refreshing the page.

updateTodo clientapp/labs/lab5/client.ts
export const updateTodo = async (todo: { id: number }) => {
  const response = await axios.put(`${TODOS_API}/${todo.id}`, todo);
  return response.data;
};
edit and PUTapp/labs/lab5/intermediates/5-2-6-WorkingWithArraysAsync.tsx
const editTodo = (todo) => {
  setTodos(todos.map((t) => (t.id === todo.id ? { ...todo, editing: true } : t)));
};
const updateTodo = async (todo) => {
  await client.updateTodo(todo);
  setTodos(todos.map((t) => (t.id === todo.id ? todo : t)));
};
<FaPencilAlt onClick={() => editTodo(todo)} className="cursor-pointer text-blue-600" />
<input type="checkbox" checked={todo.completed}
  onChange={(e) => updateTodo({ ...todo, completed: e.target.checked })} />

5.2.6.4 Handling Errors

The exercises so far have been very optimistic when interacting with the server, but it is good practice to handle edge cases and the unforeseen. In this section we are going to add error handling to some of the routes and user interface. For instance, the exercise below throws exceptions if the items being deleted or updated do not actually exist. Errors are reported by the server as status codes, where 404 is the infamous NOT FOUND error. Additionally a JSON object can be sent back as part of the response that can be used by user interfaces to better inform the user of what went wrong.

404 handlingwebdev-server/Lab5/WorkingWithArrays.js
const deleteTodo = (req, res) => {
  const { id } = req.params;
  const todoIndex = todos.findIndex((t) => t.id === parseInt(id));
  if (todoIndex === -1) {
    res.status(404).json({ message: `Unable to delete Todo with ID ${id}` });
    return;
  }
  todos.splice(todoIndex, 1);
  res.sendStatus(200);
};
const updateTodo = (req, res) => {
  const { id } = req.params;
  const todoIndex = todos.findIndex((t) => t.id === parseInt(id));
  if (todoIndex === -1) {
    res.status(404).json({ message: `Unable to update Todo with ID ${id}` });
    return;
  }
  todos = todos.map((t) => (t.id === parseInt(id) ? { ...t, ...req.body } : t));
  res.sendStatus(200);
};

In the user interface we can catch the errors by wrapping the request in a try/catch clause as shown below. If the request fails with an HTTP error response, then the body of the try block is aborted and the body of the catch clause executes instead. The exercise below declares an errorMessage state variable that we populate with the error from the server if an error occurs. The error is rendered as a red alert box using Tailwind — the PDF used Bootstrap alert-danger; the live sample uses bg-red-100 and text-red-800. To test, remove an item using the http://localhost:4000/lab5/todos/:id/delete GET route and then try to update or delete the same item using the user interface. Confirm you get an error if you try to delete or update a todo that does not exist.

try/catchapp/labs/lab5/intermediates/5-2-6-WorkingWithArraysAsync.tsx
const [errorMessage, setErrorMessage] = useState(null);
const deleteTodo = async (todo) => {
  try {
    await client.deleteTodo(todo);
    setTodos(todos.filter((t) => t.id !== todo.id));
  } catch (error) {
    setErrorMessage(error.response.data.message);
  }
};
{errorMessage && (
  <p id="wd-todo-error-message" className="rounded bg-red-100 px-3 py-2 text-red-800">
    {errorMessage}
  </p>
)}
WorkingWithArraysAsynchronouslyapp/labs/lab5/intermediates/5-2-6-WorkingWithArraysAsync.tsx

Working with Arrays Asynchronously

Todos


    Use this checklist to confirm Lab 5 covers the HTTP topics in §5.2.1§5.2.6. Each topic is listed once, with Lab, On your own, and With AI nested as a/b/c when that section has those blocks. The walkthroughs above are the source — this list is for checking coverage.

    1. Environment variables (§5.2.1)
      1. Lab componentDeclare NEXT_PUBLIC_HTTP_SERVER, wrap it in httpServer(), and use it on the Welcome link.
      2. On your ownClick Welcome and confirm the Express greeting — not a Next.js page.
      3. With AIUse process.env.NEXT_PUBLIC_HTTP_SERVER (or httpServer()) and keep id wd-welcome-link — do not hard-code localhost:4000.
    2. Path parameters (§5.2.2.1)
      1. Lab componentImplement add and subtract path routes and matching UI links.
      2. On your ownImplement multiply and divide path routes and matching links with ids starting wd-path-parameter-.
    3. Query parameters (§5.2.2.2)
      1. Lab componentImplement a query-string calculator that reads operation, a, and b from req.query.
      2. On your ownAlso handle multiply and divide as query operations.
    4. Path and query on your own (§5.2.2.3)
      1. Lab componentRepeat multiply and divide on both path and query so both UIs stay in parity.
    5. Remote objects (§5.2.3)
      1. Lab componentWork with a remote assignment object: get, edit title, and related object routes.
      2. On your ownAdd a module object at /lab5/module, Get Module Name, and routes that edit assignment score/completed and the module description.
      3. With AIAsk the assistant for a /lab5/module URL checklist — you still write the routes.
    6. Remote arrays (§5.2.4)
      1. Lab componentFetch and mutate the remote todos array from the Lab 5 UI.
      2. On your ownImplement /lab5/todos/:id/completed/... and .../description/... routes plus matching UI.
    7. Asynchronous axios (§5.2.5)
      1. Lab componentLoad data with axios on mount and update an assignment title asynchronously.
      2. On your ownChange the title, click Update Title, and confirm a refresh persists.
    8. JSON in the HTTP body (§5.2.6)
      1. Lab componentCreate, delete, and update todos with POST / DELETE / PUT and show errors.
      2. On your ownDelete a todo with the GET /delete link, then try the X (HTTP DELETE) on the same id and confirm the 404 alert.
      3. With AIKeep the deleteTodo try/catch and errorMessage — do not strip error handling.

    5.3 Next.js Server Routes

    Slides

    Express is this chapter's spine: a separate process, a separate host, a sibling webdev-server project that Lab 5 and Kambaz already talk to on port 4000. Route Handlers are the other server model — useful, not a replacement for Lab 5. Next.js API routes provide a powerful, built-in way to create server-side endpoints directly within a Next.js application, eliminating the need for a separate backend server in many cases. Defined by simply creating files inside the app/api/ directory, each file automatically becomes an API endpoint. For example, a file at app/api/hello/route.ts becomes accessible at /api/hello. These routes support all standard HTTP methods (GET, POST, PUT, DELETE, and so on) and give developers full control over the request and response objects, making it straightforward to handle form submissions, authentication, database operations, third-party API proxying, or any custom server logic — all while staying inside the same Next.js project.

    You stay inside the App Router when you do not need an independent API process. These LiveDemos fetch /api/lab5/... on the Next.js origin — they work with only npm run dev. Express Lab 5 still needs the companion on 4000. Same-origin /api does not need CORS, because the browser considers the page and the handler one origin.

    The example below demonstrates a trivial hello world route that responds with a simple JSON object when accessing http://localhost:3000/api/lab5/hello. Create app/api/lab5/hello/route.ts and confirm the browser displays the JSON message. This greeting is not the Express /lab5/welcome; it is a same-app handler that Next.js serves on port 3000.

    helloapp/api/lab5/hello/route.ts
    export async function GET() {
      return Response.json({ message: "Hello from Lab 5 API!" });
    }

    Exporting a function named GET is how the App Router maps the HTTP method to the file. Response.json sets the Content-Type and serializes the object. Older PDF listings used NextResponse.json from next/server; the live sample uses the Web Response constructor, which Next.js 16 accepts the same way. To practice the hello route, confirm the LiveDemo below fetches that JSON and prints the message without leaving the Labs page.

    HelloRouteapp/labs/lab5/intermediates/5-3-1-HelloRoute.tsx

    GET /api/lab5/hello (Next.js Route Handler)

    {}

    5.3.1 Next.js Calculator Web API

    To practice Next.js API routes in the App Router, let us implement a simple yet practical Web API that defines a basic calculator endpoint. Create a route in app/api/lab5/calculator/route.ts that implements a server-side calculator accessible via a GET request at the URL path /api/lab5/calculator. The handler reads query parameters from the request URL — a, b, and operation — performs basic arithmetic (addition or subtraction to start), validates the inputs, and returns a JSON response with the operands, operation, and computed result.

    In Next.js 16 the search string lives on request.nextUrl.searchParams rather than constructing a new URL(request.url), but the meaning is the same: read three strings, parse the numbers, and decide what to do. Invalid inputs like missing numbers or an unrecognized operation return error messages with the appropriate status code — 400 for a bad request. A request to http://localhost:3000/api/lab5/calculator?a=10&b=5&operation=add should produce { "a": 10, "b": 5, "operation": "add", "result": 15 }. Extend the switch with multiply and divide so the four operations match the Express calculator you already built in Lab5/QueryParameters.js.

    calculatorapp/api/lab5/calculator/route.ts
    export async function GET(request: NextRequest) {
      const a = parseFloat(request.nextUrl.searchParams.get("a") ?? "");
      const b = parseFloat(request.nextUrl.searchParams.get("b") ?? "");
      const operation = request.nextUrl.searchParams.get("operation");
      if (Number.isNaN(a) || Number.isNaN(b)) {
        return Response.json({ error: "Invalid numbers" }, { status: 400 });
      }
      let result: number;
      switch (operation) {
        case "add":
          result = a + b;
          break;
        case "subtract":
          result = a - b;
          break;
        case "multiply":
          result = a * b;
          break;
        case "divide":
          result = b === 0 ? Number.NaN : a / b;
          break;
        default:
          return Response.json({ error: "Invalid operation" }, { status: 400 });
      }
      if (Number.isNaN(result)) {
        return Response.json({ error: "Invalid operation" }, { status: 400 });
      }
      return Response.json({ a, b, operation, result });
    }

    The component below implements a client user interface to the server calculator Web API. The React client component is marked "use client" and provides an interactive frontend that integrates with the server through the /api/lab5/calculator endpoint we built earlier, demonstrating a complete full-stack workflow of Next.js API routes. The component uses React's useState hooks to manage inputs for two numbers (a and b), the selected operation, and dynamic states for the result and any errors. It triggers a simple fetch call to the /api/lab5/calculator route whenever the user switches operations via the dropdown or clicks the Calculate button. Parameters are encoded as query parameters. JSON responses are parsed and displayed as formatted output such as 3 + 5 = 8 in a read-only field. Tailwind classes style the inputs — the PDF used Bootstrap form-control and btn-primary; the live sample uses rounded borders and a blue button so it matches the rest of Lab 5.

    Create the CalculatorNextWebApiClient component (this book keeps it under app/labs/lab5/intermediates/) and import it from the Lab 5 page. Confirm the browser displays as shown. Type two numbers, choose an operation, and confirm the JSON from the route appears as an equation. Because the request stays on /api/lab5/..., you do not need Express running for this demo — only npm run dev.

    CalculatorNextWebApiClientapp/labs/lab5/intermediates/5-3-1-Calculator.tsx

    Calculator (Next.js Web API)






    Check Your Understanding

    Slides

    Check Express setup, CORS, axios, and the Next.js calculator. The practice quiz draws 10 items. It is not part of your course grade.

    5.4 Implementing the Kambaz Node.js HTTP Server

    Slides

    Kambaz is currently implemented entirely as a React application. Although various CRUD operations have been implemented to create, read, update, and delete courses and modules, these changes are not permanent and are lost when the browser is refreshed. To make the changes permanent, it is necessary to integrate the React user interface with a server that can access resources such as the file system, operating system, network, and database. In this section, server routes will be implemented to integrate the user interface with the server. In the next chapter, changes will be stored permanently in a MongoDB non-relational database.

    The URLs we register here — /api/users/signin, /api/courses, /api/courses/:courseId/modules — stay the same in Chapter 6. This chapter keeps the collections in process memory so you can see the HTTP contract without a database. The live webdev-server/Kambaz DAOs already contain later Mongo branches; ignore those until the next chapter and follow the in-memory listings below.

    5.4.1 Migrating the Database to the Server

    Slides

    Previous chapters declared a Database component to consolidate all data files into a single access point. Ideally, the data should reside on the server side or within a dedicated database. In this chapter we are going to move the data to the server, with a transition to a database in the subsequent chapter. Begin by creating a folder named Kambaz at the root of the Node.js project. Inside the Kambaz folder, create a Database directory and copy all JSON files from the React project — the same arrays you exported in §3.9.2. Then change the file extensions of the JSON files to JavaScript, for example, rename users.json to users.js and courses.json to courses.js. At the top of each newly converted JavaScript file, include an export default statement, as shown below.

    courseswebdev-server/Kambaz/Database/courses.js
    export default [
      { _id: "RS101", name: "Rocket Propulsion", number: "RS4550",
        startDate: "2023-01-10", endDate: "2023-05-15",
        department: "D123", credits: 4, description: "..." },
      // … remaining courses from Chapter 3
    ];

    Do the same for all the JSON files and update the import statements in a barrel file that re-exports them as one object. Use the same data files from previous chapters. Feel free to modify the data in the files to customize the content or meet requirements in this chapter. Ignore unnecessary data files.

    databasewebdev-server/Kambaz/Database/index.js
    import courses from "./courses.js";
    import modules from "./modules.js";
    import assignments from "./assignments.js";
    import users from "./users.js";
    import enrollments from "./enrollments.js";
    export default { courses, modules, assignments, users, enrollments };

    5.4.2 Integrating the Account Screens with the Server with RESTful Web APIs

    Slides

    The Data Access Object (DAO) design pattern organizes data access by grouping it based on data types or collections. The following Kambaz/Users/dao.js file implements various CRUD operations for handling the users array in the Database. Later sections in the chapter will create additional DAOs for each of the data arrays: courses, modules, and so on.

    Users DAOwebdev-server/Kambaz/Users/dao.js
    import { v4 as uuidv4 } from "uuid";
    export default function UsersDao(db) {
      let { users } = db;
      const createUser = (user) => {
        const newUser = { ...user, _id: uuidv4() };
        users = [...users, newUser];
        return newUser;
      };
      const findAllUsers = () => users;
      const findUserById = (userId) => users.find((user) => user._id === userId);
      const findUserByUsername = (username) =>
        users.find((user) => user.username === username);
      const findUserByCredentials = (username, password) =>
        users.find((user) => user.username === username && user.password === password);
      const updateUser = (userId, user) =>
        (users = users.map((u) => (u._id === userId ? user : u)));
      const deleteUser = (userId) =>
        (users = users.filter((u) => u._id !== userId));
      return {
        createUser, findAllUsers, findUserById, findUserByUsername,
        findUserByCredentials, updateUser, deleteUser,
      };
    }

    Like in the React project, install the uuid library in the Node.js project as shown below to generate unique identifiers when creating new instances of courses, modules, and other object instances.

    npm install uuid

    Routes post /api/users/signin, /signup, /profile, and /signout. The React client posts credentials with axios and then stores the returned user. This app uses AccountContext from Chapter 4 rather than a Redux slice — the PDF dispatched setCurrentUser into an accountReducer; here you call setCurrentUser from useAccountContext().

    5.4.2.1 Integrating the React Sign In Screen with a RESTful Web API

    DAOs provide an interface between an application and low-level database access, offering a high-level API to the rest of the application while abstracting the details and idiosyncrasies of using a particular database vendor. Similarly, routes create an interface between the HTTP network layer and the JavaScript object and function layer by transforming a stream of bits from a network connection request into a set of objects, maps, and function event handlers that are part of the client/server architecture in a multi-tiered application.

    The Node.js server uses routes to integrate with the user interface and implements DAOs to communicate with the Database. The server functions between these two layers, which is why it is often called the middle tier in a multi-tiered application. The following routes expose the database operations through a RESTful API, and the implementation of each function will be covered in the following sections. This chapter uses the Database component implemented in Kambaz/Database/index.js. Later chapters will refactor this by using an actual database.

    UserRouteswebdev-server/Kambaz/Users/routes.js
    import UsersDao from "./dao.js";
    export default function UserRoutes(app, db) {
      const dao = UsersDao(db);
      const signup = (req, res) => { };
      const signin = (req, res) => { };
      const signout = (req, res) => { };
      const profile = (req, res) => { };
      app.post("/api/users/signup", signup);
      app.post("/api/users/signin", signin);
      app.post("/api/users/signout", signout);
      app.post("/api/users/profile", profile);
    }

    Import and configure the routes in index.js and pass a reference to the database to each of the route modules. Work after CORS, session, and express.json() are configured — §5.4.3.1 fills those in.

    indexwebdev-server/index.js
    import express from "express";
    import db from "./Kambaz/Database/index.js";
    import UserRoutes from "./Kambaz/Users/routes.js";
    const app = express();
    UserRoutes(app, db);
    app.listen(process.env.PORT || 4000);

    Routes implement RESTful Web APIs that clients can use to integrate with server functionality. The signin route extracts properties username and password from the request's body and passes them to the findUserByCredentials function implemented by the DAO. The resulting user is stored in a server variable currentUser to remember the logged in user — §5.4.3 moves that value into the session so more than one browser can be signed in. The user is then sent to the client in the response. Later sections will add error handling in case the user is not found in the database.

    signin routewebdev-server/Kambaz/Users/routes.js
    const signin = (req, res) => {
      const { username, password } = req.body;
      currentUser = dao.findUserByCredentials(username, password);
      res.json(currentUser);
    };
    app.post("/api/users/signin", signin);

    In the React user interface, under app/(kambaz)/account, implement the client shown below to integrate with the user routes implemented in the server. The client function signin posts a credentials object containing the username and password expected by the server. If the credentials are found, the response should contain the logged in user. Use httpServer() so the same helper from Lab 5 points at localhost or Render.

    account clientapp/(kambaz)/account/client.ts
    import axios from "axios";
    import { httpServer } from "@/app/lib/httpServer";
    
    const axiosWithCredentials = axios.create({ withCredentials: true });
    const USERS_API = `${httpServer()}/api/users`;
    
    export const signin = async (credentials: { username: string; password: string }) => {
      const response = await axiosWithCredentials.post(`${USERS_API}/signin`, credentials);
      return response.data;
    };

    Implement a Sign in screen users can use to authenticate with the application. The component declares a state variable credentials to edit the username and password. Clicking the Sign in button posts the credentials to the server using the client.signin function. When the server responds successfully, the currently logged in user is stored with setCurrentUser from AccountContext and you navigate to the Profile screen implemented in a later section. The PDF called dispatch(setCurrentUser(user)); this book's account state is Context, not a Redux slice.

    Signinapp/(kambaz)/account/signin/page.tsx
    const { setCurrentUser } = useAccountContext();
    const signin = async () => {
      const user = await client.signin(credentials);
      if (!user) return;
      setCurrentUser(user);
      router.push("/dashboard");
    };

    5.4.2.2 Integrating the React Sign Up Screen with a RESTful Web API

    The DAO implements functions createUser and findUserByUsername. The createUser DAO function accepts a user object from the user interface and then inserts the user into the Database. The findUserByUsername accepts a username from the user interface and finds the user with the matching username. Those two functions implement the sign up operation for users to sign up to the application.

    The signup route expects a user object with at least the properties username and password. The DAO's findUserByUsername is called to check if a user with that username already exists. If such a user is found a 400 error status is returned along with an error message for display in the user interface. If the username is not already taken the user is inserted into the database and stored as the current user. The response includes the newly created user. The signup route is mapped to the /api/users/signup path.

    signup routewebdev-server/Kambaz/Users/routes.js
    const signup = (req, res) => {
      const user = dao.findUserByUsername(req.body.username);
      if (user) {
        res.status(400).json({ message: "Username already in use" });
        return;
      }
      currentUser = dao.createUser(req.body);
      res.json(currentUser);
    };
    app.post("/api/users/signup", signup);

    Meanwhile in the React user interface, implement a signup client that posts the new user to the Web API as shown below. If not already done so, implement a Sign up screen component that users can use to type their username and password, and post the credentials to the server for signing up. If the sign up is successful, store the user with setCurrentUser and navigate to the Profile screen. In the Sign in screen, create a Link to navigate to the Sign up screen. Confirm that you can sign up with a new username and password. Confirm it navigates to profile and shows the new user.

    signup clientapp/(kambaz)/account/client.ts
    export const signup = async (user: { username: string; password: string }) => {
      const response = await axiosWithCredentials.post(`${USERS_API}/signup`, user);
      return response.data;
    };
    Signupapp/(kambaz)/account/signup/page.tsx
    const { setCurrentUser } = useAccountContext();
    const signup = async () => {
      const current = await client.signup(user);
      setCurrentUser(current);
      router.push("/account/profile");
    };

    5.4.2.3 Integrating the React Profile Screen with a RESTful Web API

    In the User's DAO, implement updateUser to update a single document by first identifying it by its primary key, and then updating the matching fields in the user parameter. In the User's routes, make the DAO function available as a RESTful Web API. Map a route that accepts a user's primary key as a path parameter, passes the ID and request body to the DAO function, and responds with the updated user so the client can refresh Context.

    updateUser routewebdev-server/Kambaz/Users/routes.js
    const updateUser = (req, res) => {
      const userId = req.params.userId;
      const userUpdates = req.body;
      dao.updateUser(userId, userUpdates);
      currentUser = dao.findUserById(userId);
      res.json(currentUser);
    };
    app.put("/api/users/:userId", updateUser);

    In the React client application, add client function updateUser to send user updates to the server to be saved. In the Profile screen implement the updateProfile event handler to update the profile on the server and then setCurrentUser with the response. Add an Update button that invokes the new handler. Confirm that the profile changed by logging out and then logging back in.

    updateUser clientapp/(kambaz)/account/client.ts
    export const updateUser = async (user: { _id: string }) => {
      const response = await axiosWithCredentials.put(`${USERS_API}/${user._id}`, user);
      return response.data;
    };

    5.4.2.4 Retrieving the Profile from the Server

    When a successful sign in occurs, the account information is stored in a server variable called currentUser. The variable retains the signed-in user information as long as the server is running. The Sign in screen copies that user from the server into AccountContext and then navigates to the Profile screen. If the browser reloads, the Context state is cleared and the user appears logged out. To address this, the browser must check whether someone is already logged in on the server and, if so, update the copy in Context. Create a route on the server to provide access to currentUser as shown below.

    profile routewebdev-server/Kambaz/Users/routes.js
    const profile = (req, res) => {
      res.json(currentUser);
    };
    app.post("/api/users/profile", profile);

    Then in the React Web app, implement a function to retrieve the account information from that server route. Create a Session-style fetch that runs when Kambaz first loads — call client.profile(), then setCurrentUser from AccountContext. The PDF wrapped the app in a Redux Session component that dispatched setCurrentUser; this book already wraps Kambaz with AccountProvider in app/(kambaz)/layout.tsx. You can fetch the profile from a small client component under that provider, or from Profile itself on mount. Confirm that it works by signing in, and then from the Profile screen, reload the browser. Make sure the user information still renders correctly once sessions are configured in §5.4.3.

    profile clientapp/(kambaz)/account/client.ts
    export const profile = async () => {
      const response = await axiosWithCredentials.post(`${USERS_API}/profile`);
      return response.data;
    };

    5.4.2.5 Integrating Signout with a RESTful Web API

    Implement a route for users to sign out that resets the currentUser to null on the server. In the React user interface, add a client function that can post to the signout route. In the Profile screen refactor the signout function to invoke the signout client function, clear Context with setCurrentUser(null), and then navigate to the Sign in screen. Confirm that you can sign out and navigate to the Sign in screen.

    signout routewebdev-server/Kambaz/Users/routes.js
    const signout = (req, res) => {
      currentUser = null;
      res.sendStatus(200);
    };
    app.post("/api/users/signout", signout);
    signout clientapp/(kambaz)/account/client.ts
    export const signout = async () => {
      const response = await axiosWithCredentials.post(`${USERS_API}/signout`);
      return response.data;
    };

    5.4.3 Supporting Multiple User Sessions

    Slides

    The user authentication implemented so far is simple but supports only one signed-in user at a time. Web applications typically support multiple users signed in simultaneously. This section describes how to add session handling to the Node.js server to allow multiple users to be signed in at the same time.

    5.4.3.1 Installing and Configuring Server Sessions

    First, it is necessary to narrow down who is allowed to authenticate. Configure CORS to support cookies and restrict network access to come only from the React application. Install express-session and dotenv to maintain application sessions and read configurations from environment variables on the server.

    npm install express-session
    npm install dotenv

    In a new .env file at the root of the Node project, declare the following environment variables. Do not commit that file — §5.5.1 already listed it in .gitignore.

    SERVER_ENV=development
    CLIENT_URL=http://localhost:3000
    SERVER_URL=http://localhost:4000
    SESSION_SECRET=super secret session phrase

    In index.js, import the dotenv library to determine whether the application is running in the development environment, and configure the session as shown below. Make sure to configure sessions after configuring cors and before express.json() and the routes. In production set proxy, sameSite: "none", and secure cookies so the Vercel origin can send credentials to Render. The following configuration has been tested on Google Chrome and Apple Safari.

    sessionwebdev-server/index.js
    import "dotenv/config";
    import session from "express-session";
    const app = express();
    app.use(cors({
      credentials: true,
      origin: process.env.CLIENT_URL || "http://localhost:3000",
    }));
    const sessionOptions = {
      secret: process.env.SESSION_SECRET || "kambaz",
      resave: false,
      saveUninitialized: false,
    };
    if (process.env.SERVER_ENV !== "development") {
      sessionOptions.proxy = true;
      sessionOptions.cookie = {
        sameSite: "none",
        secure: true,
        domain: process.env.SERVER_URL,
      };
    }
    app.use(session(sessionOptions));
    app.use(express.json());

    Store req.session.currentUser on signin and signup instead of a module-level variable. The signup route retrieves the username from the request body. If a user with that username already exists, an error is returned. Otherwise, create the new user and store it in the session's currentUser property to remember that this new user is now the currently logged-in user. An existing user can identify themselves by providing credentials. The signin route looks up the user by their credentials, stores it in the session, and responds with the user if they exist; otherwise it responds with a 401. If a user has already signed in, the current user can be retrieved from the session by using the profile route; if there is no current user, return 401. Users can be signed out by destroying the session. If a user updates their profile, then the session must be kept in synch.

    session authwebdev-server/Kambaz/Users/routes.js
    const signin = (req, res) => {
      const { username, password } = req.body;
      const currentUser = dao.findUserByCredentials(username, password);
      if (currentUser) {
        req.session["currentUser"] = currentUser;
        res.json(currentUser);
      } else {
        res.status(401).json({ message: "Unable to login. Try again later." });
      }
    };
    const profile = (req, res) => {
      const currentUser = req.session["currentUser"];
      if (!currentUser) {
        res.sendStatus(401);
        return;
      }
      res.json(currentUser);
    };
    const signout = (req, res) => {
      req.session.destroy();
      res.sendStatus(200);
    };

    5.4.3.2 Configuring Axios to Support Server Sessions

    By default axios does not support cookies. To configure axios to include cookies in requests, use axios.create() to create an instance of the library that includes cookies for credentials as shown below. Then replace all occurrences of the axios library used for account and enrolled-course calls with this new version axiosWithCredentials.

    axiosWithCredentialsapp/(kambaz)/account/client.ts
    import axios from "axios";
    const axiosWithCredentials = axios.create({ withCredentials: true });
    export const signin = async (credentials: { username: string; password: string }) => {
      const response = await axiosWithCredentials.post(`${USERS_API}/signin`, credentials);
      return response.data;
    };
    export const profile = async () => {
      const response = await axiosWithCredentials.post(`${USERS_API}/profile`);
      return response.data;
    };

    Every signed-in request — profile, signout, update user, and later /api/users/current/courses — must use that instance, or the session cookie never leaves the browser and Express will treat you as anonymous.

    5.4.5 Creating a RESTful Web API for Courses

    Slides

    Previous chapters implemented CRUD operations to create, read, update and delete courses in the Kambaz Dashboard. These changes were transient and were lost when users refreshed the browser. This section demonstrates implementing a RESTful Web API to integrate the Dashboard and Courses screen with the server. The API will migrate the CRUD operations from the user interface to the server where they belong.

    The PDF stored the course list in a Redux coursesReducer. Chapter 4 already used a Zustand useCoursesStore for the same list. Once Express owns the data, Dashboard fetches on load with axios and keeps the result in component state — the Zustand seed from Chapter 4 is no longer the source of truth. The signed-in user still lives in AccountContext.

    5.4.5.1 Retrieving Courses

    Now that the Database has been moved to the server, it must be made available to the React client application through a Web API. The exercises below make the courses accessible at http://localhost:4000/api/courses for the React user interface to integrate. First implement a DAO to retrieve all courses from the Database, then a route that uses that DAO.

    Courses DAOwebdev-server/Kambaz/Courses/dao.js
    import { v4 as uuidv4 } from "uuid";
    export default function CoursesDao(db) {
      function findAllCourses() {
        return db.courses;
      }
      function findCoursesForEnrolledUser(userId) {
        const { courses, enrollments } = db;
        return courses.filter((course) =>
          enrollments.some(
            (enrollment) =>
              enrollment.user === userId && enrollment.course === course._id,
          ),
        );
      }
      return { findAllCourses, findCoursesForEnrolledUser };
    }
    CourseRouteswebdev-server/Kambaz/Courses/routes.js
    import CoursesDao from "./dao.js";
    export default function CourseRoutes(app, db) {
      const dao = CoursesDao(db);
      const findAllCourses = (req, res) => {
        res.json(dao.findAllCourses());
      };
      const findCoursesForEnrolledUser = (req, res) => {
        let { userId } = req.params;
        if (userId === "current") {
          const currentUser = req.session["currentUser"];
          if (!currentUser) {
            res.sendStatus(401);
            return;
          }
          userId = currentUser._id;
        }
        res.json(dao.findCoursesForEnrolledUser(userId));
      };
      app.get("/api/courses", findAllCourses);
      app.get("/api/users/:userId/courses", findCoursesForEnrolledUser);
    }

    In index.js import the new routes and pass a reference to the express module. Make sure to work AFTER the cors, session, and json use statements. Point your browser to http://localhost:4000/api/courses and confirm the server responds with an array of courses. Since the Dashboard displays courses a user is enrolled in, implement findCoursesForEnrolledUser as shown above. When userId === "current" the route reads the session and returns enrolled courses only — 401 if nobody is signed in.

    Back in the user interface, create app/(kambaz)/courses/client.ts that implements all the course-related communication between the user interface and the server. Start with fetchAllCourses, then findMyCourses that retrieves the current user's courses using cookies. In the Dashboard use useEffect to fetch the courses from the server on component load. Use the currentUser from AccountContext as a dependency so that if a different user logs in, the courses will be reloaded from the server. Remove Database references from the user interface since we do not need them anymore. Also initialize the courses state variable as empty since we will not have the JSON file anymore. Also remove the filtering of courses by enrollments since the server is already doing that. Restart the server and user interface to confirm that the Dashboard renders the courses the current user is enrolled in. Sign in as different users and confirm only the courses the user is enrolled in display.

    courses clientapp/(kambaz)/courses/client.ts
    import axios from "axios";
    import { httpServer } from "@/app/lib/httpServer";
    const axiosWithCredentials = axios.create({ withCredentials: true });
    const COURSES_API = `${httpServer()}/api/courses`;
    const USERS_API = `${httpServer()}/api/users`;
    export const fetchAllCourses = async () => {
      const { data } = await axios.get(COURSES_API);
      return data;
    };
    export const findMyCourses = async () => {
      const { data } = await axiosWithCredentials.get(`${USERS_API}/current/courses`);
      return data;
    };

    5.4.5.2 Creating New Courses

    Implement a route that creates a new course and adds it to the Database. The new course is passed in the HTTP body from the client and is appended to the end of the courses array. The new course is given a new unique identifier and sent back to the client in the response. When a course is created, it needs to be associated with the creator. In Kambaz/Enrollments/dao.js, implement enrollUserInCourse to enroll, or associate, a user to a course. Creating a course posts to /api/users/current/courses and enrolls the current user.

    createCoursewebdev-server/Kambaz/Courses/dao.js
    function createCourse(course) {
      const newCourse = { ...course, _id: uuidv4() };
      db.courses = [...db.courses, newCourse];
      return newCourse;
    }
    enrollUserInCoursewebdev-server/Kambaz/Enrollments/dao.js
    function enrollUserInCourse(userId, courseId) {
      const { enrollments } = db;
      enrollments.push({ _id: uuidv4(), user: userId, course: courseId });
    }
    createCourse routewebdev-server/Kambaz/Courses/routes.js
    const createCourse = (req, res) => {
      const currentUser = req.session["currentUser"];
      const newCourse = dao.createCourse(req.body);
      enrollmentsDao.enrollUserInCourse(currentUser._id, newCourse._id);
      res.json(newCourse);
    };
    app.post("/api/users/current/courses", createCourse);

    In the course client, add a createCourse function that posts a new course to the server and returns the brand new course. In the Dashboard, add an onAddCourse event handler that posts the new course and then reloads the list. Refactor the Add button to use that handler. Confirm that creating a new course updates the user interface with the added course.

    createCourse clientapp/(kambaz)/courses/client.ts
    export const createCourse = async (course: unknown) => {
      const { data } = await axiosWithCredentials.post(
        `${USERS_API}/current/courses`,
        course,
      );
      return data;
    };

    5.4.5.3 Deleting a Course

    Implement a route that removes a course and all enrollments associated with the course. First implement a deleteCourse DAO function that filters the course by its ID and then filters out all enrollments by the course's ID. In the Course's routes, implement a delete route that parses the course's ID from the URL. Remember to group callback functions at the top and the route declarations at the bottom.

    deleteCoursewebdev-server/Kambaz/Courses/dao.js
    function deleteCourse(courseId) {
      const { courses, enrollments } = db;
      db.courses = courses.filter((course) => course._id !== courseId);
      db.enrollments = enrollments.filter((enrollment) => enrollment.course !== courseId);
    }

    In the course client, add a deleteCourse function that deletes an existing course from the server. In the Dashboard, implement onDeleteCourse to use the client and then drop that course from local state. Reimplement the Delete button to use the new handler. Confirm clicking Delete actually removes the course from the Dashboard.

    deleteCourse clientapp/(kambaz)/courses/client.ts
    export const deleteCourse = async (courseId: string) => {
      const { data } = await axios.delete(`${COURSES_API}/${courseId}`);
      return data;
    };

    5.4.5.4 Updating a Course

    In the Course's DAO, implement updateCourse to update a course in the Database. First look up the course by its ID and then apply the updates to the course. In the Course's routes, implement a PUT route that parses the id of the course as a path parameter and uses the updateCourse DAO function to update the corresponding course with the updates in the HTTP request body.

    updateCoursewebdev-server/Kambaz/Courses/dao.js
    function updateCourse(courseId, courseUpdates) {
      const { courses } = db;
      const course = courses.find((course) => course._id === courseId);
      Object.assign(course, courseUpdates);
      return course;
    }

    In the course client, add an updateCourse function that PUTs an existing course. In the Dashboard, implement onUpdateCourse so that it uses the client and then swaps the old corresponding course with the new version in state. Refactor the Update button so that it uses the new handler. Confirm that clicking Update actually updates the course in the Dashboard. Fetch on load and after Add / Update / Delete so the list stays aligned with Express.

    updateCourse clientapp/(kambaz)/courses/client.ts
    export const updateCourse = async (course: { _id: string }) => {
      const { data } = await axios.put(`${COURSES_API}/${course._id}`, course);
      return data;
    };
    Dashboardapp/(kambaz)/dashboard/page.tsx

    Dashboard


    New Course

    Published Courses (0)


    5.4.6 Creating a RESTful Web API for Modules

    Now let us do the same thing we did for the courses, but for the modules. We will need routes that deal with the modules similar to the operations we implemented for the courses. We will need to implement all the basic CRUD operations: create modules, read/retrieve modules, update modules and delete modules. The main difference will be that modules exist within the context of a particular course. Each course has a different set of modules, so the routes will need to take into account the course ID for which the modules we are operating on. Nested in the UI, flat on the server: GET/POST /api/courses/:courseId/modules, PUT/DELETE /api/modules/:moduleId.

    The PDF updated a Redux modulesReducer with setModules. This book's Modules screen fetches into component state with the same client functions; Chapter 4's Zustand module store is no longer the source of truth once Express is wired.

    5.4.6.1 Retrieving a Course's Modules

    Create a DAO for the Modules to implement module data access from the Database. Start by implementing findModulesForCourse to retrieve a course's modules by its ID. Create a new routes file for the Module with a route to retrieve the modules for a course by its ID encoded in the path. Parse the course ID from the path and then use the module's DAO. In index.js, import the new route file and pass it a reference to the app and db.

    Modules DAOwebdev-server/Kambaz/Modules/dao.js
    export default function ModulesDao(db) {
      function findModulesForCourse(courseId) {
        const { modules } = db;
        return modules.filter((module) => module.course === courseId);
      }
      return { findModulesForCourse };
    }
    ModuleRouteswebdev-server/Kambaz/Modules/routes.js
    import ModulesDao from "./dao.js";
    export default function ModuleRoutes(app, db) {
      const dao = ModulesDao(db);
      const findModulesForCourse = (req, res) => {
        const { courseId } = req.params;
        res.json(dao.findModulesForCourse(courseId));
      };
      app.get("/api/courses/:courseId/modules", findModulesForCourse);
    }

    In the courses client, create findModulesForCourse to integrate the user interface with the server. In the Modules component, using a useEffect, invoke that client function and store the modules from the server. Remove the filter since modules are already filtered on the server. Confirm that navigating to a course populates the corresponding modules.

    findModulesForCourseapp/(kambaz)/courses/client.ts
    export const findModulesForCourse = async (courseId: string) => {
      const response = await axios.get(`${COURSES_API}/${courseId}/modules`);
      return response.data;
    };

    5.4.6.2 Creating Modules for a Course

    To create a new module in the Database, implement createModule in the Module's DAO. The function accepts the new module as a parameter, sets its primary key, and then appends the new module to the Database's module array. In the Module's routes, implement a POST Web API. Parse the course's ID from the path and the new module from the request's body. Set the new module's course property to the course's ID so that the module knows what course it belongs to. Respond with the new module.

    createModulewebdev-server/Kambaz/Modules/dao.js
    function createModule(module) {
      const newModule = { ...module, _id: uuidv4() };
      db.modules = [...db.modules, newModule];
      return newModule;
    }
    createModuleForCoursewebdev-server/Kambaz/Modules/routes.js
    const createModuleForCourse = (req, res) => {
      const { courseId } = req.params;
      const module = { ...req.body, course: courseId };
      const newModule = dao.createModule(module);
      res.json(newModule);
    };
    app.post("/api/courses/:courseId/modules", createModuleForCourse);

    In the user interface, implement a createModuleForCourse client function that posts new modules to the server. Encode the course's ID in the URL so the server knows what course the module belongs to. In the Modules screen, implement an onCreateModuleForCourse event handler that uses that client and then appends the created module to state. Update ModulesControls so Add uses the new handler. Confirm that new modules are created for the current course.

    createModuleForCourse clientapp/(kambaz)/courses/client.ts
    export const createModuleForCourse = async (courseId: string, module: unknown) => {
      const response = await axios.post(`${COURSES_API}/${courseId}/modules`, module);
      return response.data;
    };

    5.4.6.3 Deleting a Module

    In the Modules DAO, implement deleteModule to remove a module from the Database by its ID. In the Modules router file, implement a route that handles an HTTP DELETE to remove a module by its ID. Parse the module's ID from the path and use the DAO. In the courses client, implement deleteModule: pass it the ID of the module to be removed, encode it in a URL, and send it as an HTTP DELETE to the server.

    deleteModulewebdev-server/Kambaz/Modules/dao.js
    function deleteModule(moduleId) {
      const { modules } = db;
      db.modules = modules.filter((module) => module._id !== moduleId);
    }
    deleteModule routewebdev-server/Kambaz/Modules/routes.js
    const deleteModule = (req, res) => {
      const { moduleId } = req.params;
      dao.deleteModule(moduleId);
      res.sendStatus(200);
    };
    app.delete("/api/modules/:moduleId", deleteModule);

    In the Modules screen, implement onRemoveModule to remove the module from the server and then filter it from state. In ModuleControlButtons, update the delete attribute to use the new handler. Confirm that clicking the trashcan of a module removes it. Refresh the screen to make sure that the module is permanently deleted while Express is running.

    deleteModule clientapp/(kambaz)/courses/client.ts
    export const deleteModule = async (moduleId: string) => {
      const response = await axios.delete(`${MODULES_API}/${moduleId}`);
      return response.data;
    };

    5.4.6.4 Update Module

    In the Module's DAO, implement updateModule to update a module in the Database by its ID. First look up the module by its ID and then apply the updates. In the Module's routes file, implement an HTTP PUT request handler that parses the ID of the module from the URL and the module updates from the HTTP request body. In the client, encode the ID of the module in a URL and send the module updates in the body of an HTTP PUT. In the Modules screen, implement onUpdateModule. In the onKeyDown event handler, invoke that save when the user presses Enter. Confirm that updating the module in the user interface actually modifies the module on the server. Refresh the screen to make sure that the module has been modified.

    updateModulewebdev-server/Kambaz/Modules/dao.js
    function updateModule(moduleId, moduleUpdates) {
      const { modules } = db;
      const module = modules.find((module) => module._id === moduleId);
      Object.assign(module, moduleUpdates);
      return module;
    }
    updateModule clientapp/(kambaz)/courses/client.ts
    export const updateModule = async (module: { _id: string }) => {
      const { data } = await axios.put(`${MODULES_API}/${module._id}`, module);
      return data;
    };

    5.4.7 Assignments and Assignments Editor (On Your Own)

    In your Node.js server application, implement routes for creating, retrieving, updating, and deleting assignments. In the React Web application, create an assignment client file that uses axios to send POST, GET, PUT, and DELETE HTTP requests to integrate the React application with the server application. In the React user interface, refactor the Assignments and Assignment Editor screens implemented in earlier chapters to use the new client file to CRUD assignments. New assignments, updates to assignments, and deleted assignments should persist if the screens are refreshed as long as the server is running.

    5.4.8 Enrollments (On Your Own)

    In your Node.js server application, implement routes to support the Enrollments screen. Users should be able to enroll and unenroll from courses. In the React application, implement an enrollments client that uses axios to integrate with the routes in the server. Enrollments should persist as long as the server is running. The live courses client already exposes enrollIntoCourse and unenrollFromCourse against /api/users/:uid/courses/:cid if you want a starting URL shape.

    5.4.9 People Table (Optional)

    In your Node.js server application, implement routes to support the People screen. Users should be able to see all users enrolled in the course. Faculty should be able to create, update, and delete users. In the React application, implement a users client that uses axios to integrate with the routes in the server. User changes should persist as long as the server is running.

    5.5 Deploying RESTful Web Service APIs to a Public Remote Server

    Slides

    Up to this point you should have a working two-tiered application with the first tier consisting of a front-end React user interface application and the second tier consisting of a Node Express HTTP server application. In this section we are going to learn how to replicate this setup so that it can execute on remote servers. All development should be done in the local development environment on your personal development computer, and only when we are satisfied that all works fine locally should we make an effort at deploying the application on remote servers.

    The React Web application is already configured to deploy and run remotely on Vercel when you commit and push to the GitHub repository containing the source for the project. This section demonstrates how to configure the Node Express HTTP server project to deploy to a remote server hosted by Render (or Heroku) and then integrate the remote React Web application on Vercel with the Node Express server deployed and running on Render (or Heroku).

    5.5.1 Committing and Pushing the Node Server Source to GitHub

    First create a local Git repository in the Node Express project by typing git init at the command line at the root of the project. It is okay if the repository was already initialized. The working source in this book repo is already webdev-server/ at the Next.js root, which is convenient for LiveDemos, but delivery still wants a separate GitHub repository so graders can open the server history without the Next.js tree. In that folder run git init if you have not already.

    git init

    Configure the Git repository to disregard unnecessary files by listing them in .gitignore. Create a new file called .gitignore if it does not already exist. Note the leading period in front of the file name. The file should contain at least node_modules, but should also contain any IDE-specific files or directories. If using IntelliJ, include the .idea folder. Environment files such as .env and .env.development should also be included in .gitignore so session secrets and local URLs never land on GitHub.

    # webdev-server/.gitignore
    node_modules
    .env
    .env.development
    .idea

    Use git add to add all the source code into the repository and commit with a simple comment. Then head over to github.com and create a public repository named webdev-server. Add this repository as the origin target using the git remote command. Make sure to use your GitHub username instead of a sample account. Push the code in your local repository to the remote origin repository. Note that your default branch might be called main or master. Refresh the remote GitHub repository and confirm the code is now available online.

    git add .
    git commit -m "first commit"
    git remote add origin https://github.com/<you>/webdev-server.git
    git push -u origin main

    Work on branch a5 in both repos — the Next.js client and this new server repository. Creating a5 now means Render and Vercel can track the assignment branch instead of mixing earlier chapters into the same history. After the first push, git checkout -b a5 and push that branch as well so §5.7 can point graders at a stable pair of URLs.

    5.5.2 Deploying to Render.com from GitHub

    If you do not already have an account at render.com, create a new account to deploy the Node server remotely. From the dashboard on the top right, select Add New and then Web Service. In the New web service screen, in the Source Code option, select the Git Provider tab, search for the git repository created earlier and select it from the dropdown list. In the Name field, type the name of the application, e.g., use the same name as the GitHub repository webdev-server, or something similar. In the Build Command field type npm install. In the Start Command field type npm start or node index.js. Under the Instance Type select Free — the first request after idle can be slow while Render wakes the service. In the Environment Variables section click Add Environment Variable to create all the environment variables in the .env file, but with the production values shown below.

    • SERVER_ENV = production
    • CLIENT_URL = your Vercel origin (no trailing slash), for example https://your-app.vercel.app
    • SERVER_URL = the Render hostname without https://, for example webdev-server.onrender.com
    • SESSION_SECRET = a phrase that is not committed — not the sample string from the notes

    For the CLIENT_URL environment variable, use the URL of your React Web application deployed on Vercel, not a sample from these notes. For SERVER_URL, use the domain name in the Name field, but make sure it has the postfix .onrender.com. Note that after the application deploys, Render might modify the domain name slightly if the domain is already taken. You will need to edit the environment variable so that its value is the actual domain name. Make sure that SERVER_URL does not contain http:// or https:// as a prefix.

    Click Deploy Web Service to deploy the server. In the deploy screen take a look at the logs. You can click on Maximize to see the logs better. Look for a Build successful message. You can click on Minimize to minimize the logs. If the deployment fails, fix whatever the logs complain about, commit and push changes to GitHub, and try deploying again by selecting Deploy last commit from the Manual Deploy drop menu at the top right. If the deployment succeeds a URL appears at the top of the screen. Navigate to that URL and confirm you get the same greeting you would get locally, e.g., Welcome to Full Stack Development! Also confirm you can get the array of courses from the remote API at /api/courses. Finally, make sure you can get the list of modules for at least one of the courses, e.g., /api/courses/RS101/modules. The actual URL might be different based on the actual name you chose for the application.

    If the name chosen was not unique, Render will modify the name of the domain so that it is unique. It might append a random string at the end of the name. If this is the case, modify the SERVER_URL environment variable by clicking Environment on the left sidebar of the Render dashboard. Make sure all environment variables have the correct values and edit if necessary. To edit SERVER_URL, click Edit and replace the value with the actual domain name. Do not include the protocol https://, or extra slashes. Click Save, rebuild, and deploy when done.

    5.5.3 Configuring the Remote Environment in Vercel

    Now that the Node.js HTTP server is running remotely on Render.com, the React Web application running on Vercel needs to be configured to integrate with the server running on Render.com. Currently the React Web application is configured to connect to the local Node Express server, but when the Web application is running on Vercel it needs to connect to the remote Node Express server running on Render or Heroku. Configure Vercel by defining the environment variable NEXT_PUBLIC_HTTP_SERVER so that it references the remote server running on Render or Heroku.

    To configure environment variables in Vercel, navigate to your project. In the Overview screen, navigate to the Deployment. In the Deployment Details screen, under the Environment label, navigate to Production. In the Project Settings screen, navigate to Environment variables on the left sidebar. In the Environment Variables screen, in the Create new tab, in the Key input field, enter NEXT_PUBLIC_HTTP_SERVER, and then in the Value field, copy and paste the root URL of the application running on Render or Heroku, e.g., https://webdev-server.onrender.com. Make sure there is no trailing slash. Click Save and then Redeploy. Note that here we do want the protocol https://, but not in the SERVER_URL environment variable on Render.com. Redeploy the React application and confirm that the Dashboard renders the courses from the remote Node server and Modules still renders the modules for the selected course. Also confirm all the labs still work when running on Vercel.

    Using the Network tab in the Inspector in the Development Tools of the browser, make sure that none of the API calls still use http://localhost:4000 on the live site — the PDF also mentioned localhost:3000; the Express companion is 4000, and that is the origin that must disappear from production requests. Locally keep .env.development at http://localhost:4000 so §5.2 LiveDemos stay on the companion process. Same httpServer() helper — only the env value changes. Lab 5 LiveDemos do not need this Vercel step; they already work against http://localhost:4000. Route Handler demos in §5.3 keep using same-origin /api even if that env is unset.

    5.6 Conclusion

    In this chapter we learned how to create HTTP servers using the Node.js JavaScript framework. We implemented RESTful services with the Express library and practiced sending, retrieving, modifying, and updating data using HTTP requests and responses. We then learned how to integrate React Web applications with HTTP servers, implementing a client/server architecture: the Next.js UI is the client; Express in the sibling webdev-server project is the server.

    Lab 5 walked through path parameters, query strings, remote objects and arrays, AJAX with axios, CORS, and JSON bodies with POST, PUT, and DELETE. §5.3 showed Route Handlers as a same-app option when you do not need an independent process. §5.4 moved Kambaz courses, modules, and account screens onto those same HTTP verbs, with sessions so more than one user can be signed in. §5.5 deployed the pair — Vercel for the UI, Render (or Heroku) for Express — and pointed NEXT_PUBLIC_HTTP_SERVER at the public origin. In the next chapter we will add database support to the HTTP server so we can store data permanently; the URLs and clients stay, and MongoDB replaces the in-memory arrays.

    5.7 Deliverables

    As a deliverable, make sure you complete all the lab exercises, the course, module, and assignment routes on the server, as well as the client and component refactoring on the React project. For both the React and Node repositories, all your work should be done in a branch called a5. When done, add, commit, and push both branches to their respective GitHub repositories. Deploy the new branches to Vercel and Render (or Heroku) and confirm they integrate. All the lab exercises should work remotely just as well as locally. The Kambaz Dashboard should display the courses from the server as well as the modules and assignments.

    # in webdev-client
    git checkout -b a5
    git add .
    git commit -am "a5 HTTP APIs"
    git push -u origin a5
    
    # in webdev-server
    git checkout -b a5
    git add .
    git commit -am "a5 HTTP APIs"
    git push -u origin a5
    1. Complete every Lab 5 exercise in §5.2, including multiply and divide on both path and query, the module object on your own, todo completed and description routes, axios load-on-mount, and POST / DELETE / PUT with error handling.
    2. Implement course and module routes (and assignment routes) on Express, plus the matching clients in the React project, so Dashboard and Modules survive a refresh while the server is running (§5.4).
    3. Work on branch a5 in both repositories — the Next.js app and webdev-server.
    4. Deploy the UI to Vercel and the API to Render (or Heroku) and confirm they integrate (§5.5).
    5. The Labs TOC still lists every lab, your full name, wd-github to the Next.js repo, plus links to the Node GitHub repo and the Render (or Heroku) root URL. Style those links with the existing Tailwind Labs TOC — not Bootstrap pills.
    6. Disable Vercel Deployment Protection so graders can open the a5 preview without signing in (§1.6).
    7. In Canvas, submit the Vercel URL for the a5 branch deployment. Graders will also use the Render API and both GitHub a5 branches.

    Continue in Labs, browse Lab 5 steps, or open Kambaz. Chapter 6 replaces the in-memory arrays with MongoDB.

    5.8 References

    This chapter moved data out of the browser and onto an HTTP server. The linked terms are the Node, Express, and Next.js pieces you configured; the topics are the request shapes and server habits the labs practiced.

    These ideas also matter in this chapter even though they do not have their own term pages yet:

    • Sibling webdev-server project on port 4000
    • Path parameters, query parameters, and JSON request bodies
    • Remote objects and remote arrays
    • Asynchronous HTTP from a React client
    • Sessions and account routes
    • Environment variables for the public server URL

    5.9 Tools

    Install and deploy from these official sites. Express and Node.js are the server; axios is the client; Render hosts the API; Vercel still hosts the Next.js UI.

    • Node.jsThe JavaScript runtime you install so Next.js, npm, and later the Express server can run on your machine.
    • ExpressThe Node.js HTTP framework the sibling webdev-server uses for REST routes.
    • nodemonA development helper that restarts the Node server whenever you save a file.
    • axiosA promise-based HTTP client the React app uses to call the server.
    • Next.js Route HandlersNext.js App Router endpoints that can answer HTTP from the same project as the UI.
    • Chrome DevToolsChrome's built-in inspector for HTML, CSS, the console, and the Network panel.
    • RenderThe host for the Node/Express API so the deployed client has a public server URL.
    • GitHubThe host for your remote Git repository and the place Vercel and Render connect when you deploy.
    • VercelThe host for the Next.js client; connect the GitHub repo here to put the UI on the public Web.

    5.10 AI Tools

    HTTP status codes, CORS headers, and route order are the kind of details a coding assistant can talk through while you watch the Network panel. Treat generated Express snippets as drafts — the sibling server still has to run on your machine.

    • CursorAn AI-native editor that reads your project and helps write or refactor TypeScript in place.
    • ClaudeA conversational assistant for explaining APIs, reviewing code, and drafting implementations.
    • GitHub CopilotAn AI pair programmer that suggests code as you type in the editor.
    • Google Prompt GalleryA public collection of Gemini prompt examples you can remix for writing, coding, and multimodal tasks.