Projects People Resources Semesters Blog About

Building A RESTful API (Oasis Roster)

Author:

Last Updated: 31 December, 2022

📊 Table of Contents

ℹ️
This walkthrough extends our earlier React Profile Generator (
🦋
Building Your First Site (Profile Generator)
). While it is recommended to review that walkthrough first, it is not required.

🥅 The Goal

Build an API (Application Programming Interface) that allows you to fetch current Oasis members from a pre-determined roster. With this API, you will be able to generate profile cards on the fly for any Oasis member.

To build this, we’ll be using Express. Express is a web framework for Node.js that we can quickly and easily build an API with. We can even integrate it into our earlier React frontend!

By the end of this walkthrough, you will build an Express API backend with a simple and static “database” storing info about Oasis members. You will be able to query all of them or just a select few based on each member’s data.

📦 Setup

First, launch VS Code, setup a new repo in a blank directory, and open up your terminal to this directory. See

or
Version Control
Version Control
if you need any help with this.

Next, run npm init --yes. This will allow us to manage Node packages via NPM.

Install Express and CORS once that’s complete by running npm install express cors.

To make your life a tiny bit simpler, open up the generated package.json file and update the scripts object:

// package.json
// ...
"scripts": {
  "start": "node index.js"
},
// ...

Lastly, create an index.js file in this same root directory. This will serve as the entry point to our API. The file name must match the value of main defined within your package.json. Scaffold out this file by adding the following:

// index.js
const express = require("express"); // import Express
const app = express(); // set up our Express app instance
const port = 8000; // set the port for our API to run on

app.get("/", (req, res) => { // create a GET endpoint at `/`
  res.send("Hello World!"); // return value of the endpoint
});

app.listen(port, () => { // listen for the port specified above
  console.log(`API listening on port ${port}`); // print a success message to the terminal
});

Your backend is now ready to go 🎉. Launch a development server at any time by running npm start from your terminal at the root of your API directory. Open up localhost:8000 in your browser — you should see “Hello World” displayed!

With that all handled, you can now start implementing your custom API!

👥 Setting Your Roster

Let’s build out a roster! We’ll need this data in order to query it later on.

In the root of your API directory, create a new data directory with a members.js file. We’ll use this to store a JavaScript array of objects containing our Oasis members.

Within this file, create a const named MEMBERS. Per our Profile Generator, each member should have a photo, name, title, and fun fact. We’ll assume that all photos are public URLs (strings) accessible on the web for simplicity.

It is best practice to assign a UUID (universally unique identifier) to each record. This lets us differentiate, for example, two members with the same name. We can do this by adding a new id field to our member model. See

for more info.

Now, here’s how we can represent our members:

// members.js
const MEMBERS = [
  {
    id: 1,
    photo: "https://course.ccs.neu.edu/cs2500/_custom/img/assistant/andersonf.jpg",
    name: "Frank Anderson",
    title: "Program Coordinator",
    funFact: "Is neither totally left handed nor totally right handed.",
  },
  {
    id: 2,
    photo: "https://course.ccs.neu.edu/cs2500/_custom/img/assistant/_jsella.jpeg",
    name: "Jay Sella",
    title: "Mentor",
    funFact: "I listened to over 2,500 artists across 62 genres last year.",
  },
  // any other members ...
];

module.exports = MEMBERS;
Feel free to add as many members as you wish! Just make sure to follow the same template and ensure each member has a unique id.

🚏 HTTP Request Methods

HTTP (Hypertext Transfer Protocol) defines a set of universal request methods to indicate the desired action to be performed for a given resource. While there are many of these, the most common are GET, POST, PATCH, and DELETE.

GET

POST

The POST method is used to send information to a server. These requests often include data in the body of the request, such as a JSON object. Examples of POST requests include the data of a member to add them to Oasis’ roster or creating a new comment on a post. More info:

PATCH

The PATCH method is used to apply partial modifications to an existing resource. For example, if the title of an Oasis member changes, a PATCH request could be sent with the new title to update that piece of the desired member’s data. More info:

DELETE

📚 GETting All Members

Let’s say we want to display a gallery of all Oasis members. To do this, we’ll need a way to access our list of members. We can do this by creating a GET API endpoint at /members. This path is a good choice because it describes the data we are fetching.

Let’s update our index.js to import our MEMBERS and add this new route:

// index.js
const express = require("express"); // import Express
const MEMBERS = require("./data/members"); // import our roster

const app = express();
const port = 8000;

app.get("/members", (req, res) => { // create a GET endpoint at `/members`
  res.send(MEMBERS); // return all of our `MEMBERS` as an array of JSON objects
});

// ...

Once you open up localhost:8000/members in your browser, you should see a JSON response like the below. The data returned will match your members.js file. If not, make sure that file is saved and your browser is refreshed! There is some more info on JSON under

.

[
  {
    "id": 1,
    "photo": "https://course.ccs.neu.edu/cs2500/_custom/img/assistant/andersonf.jpg",
    "name": "Frank Anderson",
    "title": "Program Coordinator",
    "funFact": "Is neither totally left handed nor totally right handed."
  },
  {
    "id": 2,
    "photo": "https://course.ccs.neu.edu/cs2500/_custom/img/assistant/_jsella.jpeg",
    "name": "Jay Sella",
    "title": "Mentor",
    "funFact": "Lorem ipsum"
  }
]
A sample JSON API response returned from GET /members based on the data defined in members.js above.
🔃
Not seeing your changes? Unlike create-react-app, Express doesn’t support hot reloading out-of-the-box. You’ll need to restart your development server from the command line often in order to see your changes. To do this, navigate to the same terminal where you ran npm start to start your Express server. Press Ctrl+c to shut down the server. Restart it by running npm start and refresh your browser. If you’d like to add this functionality, one option is to install nodemon via NPM. This will keep you from needing to restart your server, but you’ll still need to refresh your browser manually!

🔍 GETting A Specific Member

While being able to retrieve all of our members is helpful, what if we wanted to look up someone specific by their unique ID? Maybe we want a more detailed profile page or to reference them from another record like the author of a comment.

Creating the Endpoint

To do this, we can create a new GET endpoint at /members/:id. This :id notation represents a route parameter named id. Route parameters are named segments of a URL used to capture caller-specified values at that location in the URL. For example, we could make a GET request to /members/1 or /members/oasis. The id for each of those requests would be 1 and oasis, respectively.

We can access these parameters within the req object:

// index.js
// imports and Express setup ...

app.get("/members/:id", (req, res) => { // create the GET endpoint
  const { id } = req.params; // destructure and store our `id` parameter
  res.send(id); // return the ID
});

// other endpoints ...
💡
Above, req.params is destructured to access its id field and store that as its own variable. This type of assignment can be helpful when using or re-using multiple parameters from the same object. Alternatively, this destructuring could be skipped and the field accessed directly: res.send(req.params.id).

So we can now take in any member ID and return it to the user. However, that isn’t super useful for our API. Let’s update this to look up the specified member from our roster.

Fetching the Requested Member

Similar to how you accessed and returned your entire roster in the first endpoint, we can filter this to only return one that matches our given id route parameter. This follows the same basic logic as how the filter list abstraction works with lambda in Racket.

app.get("/members/:id", (req, res) => {
  const { id } = req.params;
  const member = MEMBERS.filter((m) => m.id == id); // find all members whose `id` match the given `id`
  res.send(member); // return the member
});

If your roster matches the sample provided above, visiting localhost:8000/members/0 should show you Frank’s information (Frank’s id is 0).

Handling Errors

Non-existent Member

What should happen if the requested member doesn’t exist in our roster? Currently, an empty array ([]) is returned. This is the built-in handling of filter().

Suppose we want to return an error message instead. We can do that by first checking the length of the member array and then conditionally returning an error message:

app.get("/members/:id", (req, res) => {
  const { id } = req.params;
  const member = MEMBERS.filter((m) => m.id == id);
  
  if (member.length === 0) { // no member matches the given `id`
    res.status(404) // return an HTTP status of 404 (not found)
       .send({ error: "Member not found" }); // return an error message
  } else {
    res.send(member); // return the found member
  }
});
💡
There is an important difference between == (line 3 above) and === (line 5 above). == (equality operator) compares the value of two variables while === (strict equality operator) compares the value and type of two variables. For example:
1 == 1     // true
1 == "1"   // true
1 === 1    // true
1 === "1"  // false

Above, the route parameter id is a string but each member’s id is a number. Comparing these in their current state would always evaluate to false, so we’d never be able to look up the desired member. We’ll improve this in the next section.

Invalid ID

Right now, any string can be passed as an id. However, per our roster model, IDs should only be numbers (integers, specifically). We can implement handling to convert id to an integer and return an error if invalid:

app.get("/members/:id", (req, res) => {
  const { id } = req.params; // 
  const parsedId = parseInt(id); // attempt to convert the given `id` to an integer and store the variable
  
  if (!Number.isInteger(parsedId)) { // determine if `id` is an integer
    return res.status(400) // return an HTTP status of 400 (bad request)
              .send({ error: "Invalid `id` provided" }); // return an error message
  }

  const member = MEMBERS.filter((m) => m.id === parsedId); // compare the new `parsedId` with strict equality
  if (member.length === 0) {
    return res.status(404).send({ error: "Member not found" });
  }

  return res.send(member);
});
Note: the type of id is checked before looking up the members. This avoids evaluating unnecessary code in the event of an error and prevents potential errors if filtering by an invalid id. This also now supports strict equality (===) since parsedId must be a valid integer at that point.

🚦 CORS

CORS, short for cross-origin resource sharing, is a mechanism that allows resources such as an API endpoint to be accessed from domains other than its own host. In our case, this Express app (at localhost:8000) is separate from any frontend user interface. That frontend will likely be accessed at a different domain (eg, localhost:3000). So, if attempting to fetch backend data from our frontend, default CORS settings would prevent this and return a CORS error.

However, we can change this handling! During setup, you should’ve installed the CORS NPM package. Setting it up here is simple. Import the package and apply it to all routes:

// index.js
const express = require("express");
const cors = require("cors"); // import CORS
const MEMBERS = require("./data/members");

const app = express();
const port = 8000;

app.use(cors()); // apply this CORS handling to all endpoints

// API endpoints ...

This configuration is super simple and should cover most general use cases. In production, you’ll likely want to specify known “origins” that you control, allowing only those to fetch data from your API. You can learn more about options and instructions in the official CORS docs for Express:

🧨 Taking This Further

Here are some ideas of how you could extend this based on what you’ve learned so far:

  • add additional data to your member model (see:
    💽
    Data Design
    )
  • implement more robust/verbose error handling (eg, a dedicated error message if providing a negative number)
  • create a new endpoint that lets you filter members by any number of caller-specified specified tags (eg, student year, project groups, courses)
  • build a new endpoint with fuzzy search to support looking up members by their name
  • hook this backend up to a React frontend (see:
    🥪
    Going Full-Stack (Profile Generator)
    )
  • connect to a local database that supports creating, updating, and deleting data (eg, Firebase. See
    💽
    Data Design
    and
    Firebase Tutorials & Resources
    Firebase Tutorials & Resources
    )
  • store profile images locally and serve them from your Express app
  • anything else!