Author:
Last Updated: 31 December, 2022
📊 Table of Contents
- 📊 Table of Contents
- 🥅 The Goal
- 📦 Setup
- 👥 Setting Your Roster
- 🚏 HTTP Request Methods
- GET
- POST
- PATCH
- DELETE
- 📚 GETting All Members
- 🔍 GETting A Specific Member
- Creating the Endpoint
- Fetching the Requested Member
- Handling Errors
- Non-existent Member
- Invalid ID
- 🚦 CORS
- 🧨 Taking This Further
🥅 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
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!
As you build out your API, you may want to reference official guides and documentation for Express:
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
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;
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.
Many of these methods have additional optional parameters, such as content types and request bodies. MDN provides a good rundown on all request methods:
GET
The GET method is used to request information from a server. It should not be used to send any data. For example, it could be used to retrieve Oasis’ roster, a member’s profile data, or a weather forecast for a given location. More info:
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
The DELETE method is pretty straightforward — it deletes the specified resource! Use cases could include removing a member from Oasis’ roster or deleting a comment from a post. More info:
📚 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"
}
]
members.js
above.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 ...
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
}
});
==
(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);
});
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 andData Design)Firebase Tutorials & Resources
- store profile images locally and serve them from your Express app
- anything else!