DEV Community

Cover image for Implementing your first REST API with Express and Node.js
Bruno da Silva
Bruno da Silva

Posted on

Implementing your first REST API with Express and Node.js

This article walks you through building your first REST API as a backend component you can deploy on the web. The target audience is beginners in either REST APIs or Express/Node.js frameworks. In the end, you'll have a chance to evolve the code by yourself. Use it to self-assess if you need further study on this matter or seek help.

This article was initially created as a lab assignment in an intro to software engineering course I've taught at Cal Poly. Follow me, and soon you'll see a series of other articles associated with this project, such as a React frontend app to consume this API, database connection, testing, and continuous integration. Stay tuned.

We'll build our backend in JavaScript (Node.js) with a framework called Express.js. Express is a lightweight web development framework that is straightforward to build REST APIs for web applications and beyond.

Node.js and Express logos

0 - Installing Express.js

This article assumes you already have Node.js installed on your computer. If you don't, please go to nodejs.org, download and install it. To install Express, create a folder for your backend project and move to that folder. For instance, on your terminal, run:

mkdir expressjs-backend
cd expressjs-backend

Now, we need to run the npm init command to make this folder a Node.js project folder. A Node.js project folder is characterized by a package.json file (among other elements we'll see).

npm init

This command prompts you for several items, such as the name and version of your application. For now, you can simply hit RETURN to accept the defaults for most of them, with the following exception: entry point. Enter backend.js, or whatever you want the name of the main file to be. If you want it to be index.js, hit RETURN to accept the suggested default file name. At any time, you can edit your package.json file and modify that info.

To conclude this step, we'll use npm to install express:

npm install express

At this point, your project folder has the package.json file (and a package-lock.json file) with the express dependency you just installed and the /node_modules folder that will aggregate all dependencies needed to build and run your project.

1 - Creating a Hello World

First, let's create a hello world in Express to ensure we have all the environment set up to continue.

Continuing on the same folder from the previous step, create the main file for your backend service (your REST API). I named my main file backend.js (see the last step). So I'll create this file and open it on my code editor. If your main file is index.js, proceed with creating this file accordingly.
A minimal Express application looks something like this:

const express = require('express');
const app = express();
const port = 5000;

app.use(express.json());

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

So, what does that code do?

  1. First, we imported the Express module. Express will work as an HTPP middleware dispatching HTTP calls to the routes we define in the file and also sending back responses that we'll program.
  2. Next, we create an instance of Express and define a constant to represent the port number we'll use to listen to incoming HTTP requests.
  3. On the fourth line, we set up our express app to process incoming data in JSON format. With that, Express (as a middleware) will allow us to access JSON data seamlessly in memory.
  4. Then, we set up our first API endpoint with the app.get function. This sets the endpoint to accept http GET requests. And the app.get has two arguments. First, '/' is the URL pattern that will map to this endpoint. Second, we have the callback function that'll be called when our server receives an incoming GET request matching the '/' URL pattern. This anonymous function receives two objects, one representing the request (req) and the other representing the response (res). Inside the function, we can use those objects to process the request and send a response to the client that called the REST API. For now, we just use the response object to send a msg back.
  5. Finally, we make our backend server listen to incoming http requests on the defined port number.

To run the application, type the following command on the terminal:

node backend.js [or whatever filename you used]

And you should see the following output on the terminal:

Example app listening at http://localhost:5000

Now head over to http://localhost:5000/, and you should see your hello world greeting since that represented a request to the '/' (root) route of your backend.

If you get errors at this point, they will most likely be: (i) syntax errors on the JavaScript file; (ii) fail to install the express package or node.js environment.

Before moving to the next step, I recommend that you initialize this folder with git (git init command in the folder) to start version-controlling it with git and later push changes to GitHub. After running git init, create a .gitignore file to ignore your /node_modules folder and prevent it from being uploaded to GitHub when you push your changes. Just add the '/node_modules' to an empty .gitignore file and save it. In rare situations, you would want your /node_modules folder uploaded. In most cases, having your /node_modules on the remote repo is a mistake.

You can also create a GitHub repo for this project. Stage and commit your changes. And if you have a GitHub repo, push your changes to remote.

2 - Beyond the "/" route

Our "/" route mapped to a simple "Hello World" string response. Still, it's exposed as a REST API. Essentially, any application written in any language can make a GET http request to your API endpoint and obtain the http response with the Hello World message. Although this might not sound useful, the infrastructure is already set up, which is very helpful.
Let's now make a new route to allow clients to retrieve a list of "users".

Here's the data structure to add to your JavaScript file:

const users = { 
   users_list :
   [
      { 
         id : 'xyz789',
         name : 'Charlie',
         job: 'Janitor',
      },
      {
         id : 'abc123', 
         name: 'Mac',
         job: 'Bouncer',
      },
      {
         id : 'ppp222', 
         name: 'Mac',
         job: 'Professor',
      }, 
      {
         id: 'yat999', 
         name: 'Dee',
         job: 'Aspring actress',
      },
      {
         id: 'zap555', 
         name: 'Dennis',
         job: 'Bartender',
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode

This is in the form of a JSON object.

Now, we'll define the '/users' GET route which will return the entire list of users. Here's the code to add to your JavaScript file.

app.get('/users', (req, res) => {
    res.send(users);
});
Enter fullscreen mode Exit fullscreen mode

Again, head over to http://localhost:5000/, and you should see the response content in json format.

Git-commit current changes and continue. Push changes to GitHub if you've already set up a remote repo there.

3 - Using nodemon and avoid having to restart the server manually

At this point, you may have noticed that you need to manually kill the server and restart it again every time you change the code. This is very inconvenient while you're programming and making changes to your code constantly. Then, we'll install a package called 'nodemon' and use it to run our app backend to overcome this scenario. On the root of your project folder, run:

npm install --save-dev nodemon

We're using the save-dev argument because this is a project dependency only needed in the development environment. This is not a necessary dependency at runtime in a production-like environment. You can look at your package.json file and note a separate section for dev dependencies.

Now, instead of running your backend app with 'node', we can run it with nodemon. Nodemon will execute node, but beyond that, it'll restart the server every time you save changes on the code.

nodemon backend.js

Make any changes to your code and note nodemon automatically restarting the server.

Another good practice is to edit the package.json file and add two new script targets for running the app. In your package.json file, the "scripts" section will look like this:

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon backend.js",
"start": "node backend.js"
},

Now, if you type in your console "npm run dev" you'll run the backend app with nodemon. And if you type "npm start" you'll use node. Typically, you use "npm run dev" while coding your backend, and "npm start" will be used when you want to run your app in the production environment or as part of a CI/CD pipeline (out of this article scope). These changes won't make much difference for now, but they will make much more sense when we start working with continuous integration & deployment. For now, this step helps you explore a bit more the package.json file and become more familiar with it.

4 - Getting users by name

Suppose that, besides fetching all users, we want to retrieve users with a certain name. To do that, we do not need to create a new route. We can use http query strings to pass a name as an argument through the API and access it in our '/users' GET endpoint. If our query argument is name, then we can access it calling req.query.['name'] or req.query.name
In the browser (or any other web client), the URL will be …/users?name=Mac, where Mac is an example of a name I'm looking for.

Now we know how to pass an http query argument as part of a GET method, with a bit of logic we can find specific users with a given name to return in our get users endpoint. A possible solution for that is this one:

app.get('/users', (req, res) => {
    const name = req.query.name;
    if (name != undefined){
        let result = findUserByName(name);
        result = {users_list: result};
        res.send(result);
    }
    else{
        res.send(users);
    }
});

const findUserByName = (name) => { 
    return users['users_list'].filter( (user) => user['name'] === name); 
}
Enter fullscreen mode Exit fullscreen mode

And Step 4 is done! On your browser, access http://localhost:5000/users?name=Mac, and you should see the response containing the two users with the name 'Mac'. Take the chance to play around with different values for the query argument name.

[Git-commit current changes and continue]

5 - Getting users by ID

The way you define your API routes is relevant. A good practice is to avoid action names (e.g., get-users) when dealing with basic operations (create, retrieve, update, delete) over an entity-set of your application (in our case 'users'). In our example, everything should be over the route '/users' or variations of it '/users/', which we'll explore in this current step.

This other article gives you more background on REST API design, including how you should name the API operations by properly leveraging the http methods (GET, POST, PUT, DELETE, PATCH). It's out of the scope of this article to cover everything about web API design.

In this step, we'll address the '/users/:id' GET endpoint which should be used when we want to retrieve a specific user by the given id. Here, we're assuming the id field uniquely identifies a user. Express allows you to use the ':' symbol to mark a variable that is part of the URL (don't confuse it with query arguments explored in the previous step). This variable will be then passed to the endpoint function you create to handle the requests coming from that route. Since our route is '/users/:id', we'll create a function accordingly.

With a bit of logic, we can find the right user with a given id. Otherwise, we respond with a 404 resource not found msg.

app.get('/users/:id', (req, res) => {
    const id = req.params['id']; //or req.params.id
    let result = findUserById(id);
    if (result === undefined || result.length == 0)
        res.status(404).send('Resource not found.');
    else {
        result = {users_list: result};
        res.send(result);
    }
});

function findUserById(id) {
    return users['users_list'].find( (user) => user['id'] === id); // or line below
    //return users['users_list'].filter( (user) => user['id'] === id);
}
Enter fullscreen mode Exit fullscreen mode

Note that we can use the JavaScript Array find() function for this specific intent instead of filter(). Find returns the first occurrence that matches the condition. Since we're assuming id is a unique identifier, the find() function is a possible solution, too.

Also, note the syntax to return a specific http status code (we chose 404) along with a string msg.

On your browser, access http://localhost:5000/users/zap555, and you should see only Dennis, the bartender.

[Git-commit current changes and continue]

6 - Using the POST method

With the previous endpoints, we've been reading from the backend. Now, consider we need to add a new user to the list of users. That's our first 'write' operation.

Note: we're working with data in memory, so our add operation will not be persistent. However, the API design is the same if we would implement data persistence with a database.

Again, as I said in the previous step, the route is the same: '/users'. Don't make a '/add-user' route (although that would work, it's generally a bad design). Instead, we'll reuse the same URL pattern (/users) but now handling a different action since it'll be a POST endpoint. If the http request comes as a GET request, it will do what we already did in the previous step. If the http request comes as a POST request, we'll get the json data that comes in and add it to the list.

Note: intentionally, we're not doing data validation for the sake of simplicity. For beginners, there's already a lot to bite in this work. If you feel it's still too basic for you, take data validation as a stretch goal, think about what data validation you can implement on this endpoint, pick the right http code for responding in case of invalid data, and go with it.

Express.js allows us to implement endpoints for any HTTP method (GET, POST, PATCH, PUT, DELETE, etc.). We've used GET, now we'll use POST. For now, we won't handle different HTTP codes and unsuccessful addition. Let's just assume the data is in the right format and will always be inserted. Note in the code below how we send an empty response with a given HTTP code. 200 is the default response code, but it was explicitly defined in the code below to show you how we can pass any HTTP code we want.

app.post('/users', (req, res) => {
    const userToAdd = req.body;
    addUser(userToAdd);
    res.status(200).end();
});

function addUser(user){
    users['users_list'].push(user);
}
Enter fullscreen mode Exit fullscreen mode

Note how we can simply access the incoming data in the request body.

To test this step, we have to make a POST call that can't be done through the browser without an additional tool (since we need to attach a json object to be added once it gets through the API - our new user). There are several options to test our POST endpoint. One option is to install a browser add-on such as Boomerang for Chrome or Rested for Firefox, and by using their user interface you can make API calls attaching json objects manually. For instance, use this new json object to be added to the backend users list:

{
  "id": "qwe123",
  "job": "Zookeeper",
  "name": "Cindy"
}
Enter fullscreen mode Exit fullscreen mode

Note the double-quotes. Even though JavaScript accepts single quotes besides double quotes (or no quotes at all for the keys in the key-value pairs), the outside world does not know you're implementing your backend in JavaScript. Besides that, we have to follow the JSON standard, which is "double quotes." It will throw a 400 bad request if you pass the json object in single quotes.

Besides using browser add-ons, another option is to install a command line tool called curl. With curl, you can make all sorts of API calls in the console text interface. Finally, you can also use more robust and complete API dev tools such as Postman.

Once you make the POST call, to finally add a new user to the list, you can make a fetch call and see your recent user added to the list.

Don't forget to git-commit current changes. If you haven't pushed to a remote server, it's also a good time to do it.
Further documentation and tutorials around Express, go to https://expressjs.com/.

7 - Now evolve the code by yourself

With what we've produced so far, you should be able to evolve the code by yourself. Here I have two tasks for you:

First, implement a hard delete operation to remove a particular user by id from the list. Hint: look at another http method called DELETE. And remember: always try to reuse existing URL patterns. Don't implement a route named '/delete-user'. Look at the resource you want to access and perform the delete action (you may have an URL pattern for it already).

Second, implement an additional action to get all users that match a given name and a given job. Hint: look at what we did in step 4 and extend it.

Don't forget to commit and push your code.

Have fun with Express and Node.js!

Notes: There are many other ways to implement what we've done in this article. Some of the choices made here have educational purposes in mind, while others are entirely arbitrary and could be replaced or avoided. If you want to discuss anything related to this content, please let me know by reaching out to me on Twitter (@BrunoDaSilvaSE) or dropping a comment below.

I welcome your feedback!

Discussion (1)