Building REST API endpoints with Node.js & Express

Building REST API endpoints with Node.js & Express

REST APIs, or Representational State Transfer Application Programming Interfaces, enable data transmission over network requests. In modern industry, they are commonly used to facilitate communication between the frontend and backend, with API requests typically sent from the frontend to the backend via a REST API.

HTTP Verbs / Request Methods

REST APIs make use of HTTP verbs such as GET, POST, PUT, DELETE, and others. In this tutorial, we will focus on the most commonly used ones. Let’s briefly explore these popular verbs that we will be using in this project:

  • GET: Used exclusively to retrieve data in response to a request.
  • POST: Allows sending data in the request body and also receiving data in the response.
  • PUT: Similar to POST but primarily used for updating existing data.
  • DELETE: Used to remove data.

For more detailed information on these HTTP Verbs, please refer to the official documentation on MDN.

Response Status Codes

Another important concept we need to address is response status codes. These codes are returned along with the server’s response to a request. For instance, when you send a request to a server, the outcome may vary, leading to different status codes such as the following:

  1. The request is successful, and the response is as expected.
  2. The request fails due to errors in the request on the client side.
  3. The request is successful, but the server is not responding.
  4. The request and response are successful, but the client is redirected.

While there are additional scenarios to consider, response status codes are limited to a range from 100 to 599. These codes are categorized as follows:

  • 100 – 199: Informational responses
  • 200 – 299: Successful responses
  • 300 – 399: Redirection messages
  • 400 – 499: Client error responses
  • 500 – 599: Server error responses

For more detailed information on these responses, please refer to the official documentation on MDN.

What we will be building

In this tutorial, we will develop the Foodie Backend application using Express by adding new APIs. To get started, let’s clone the project by following these steps:

Clone the project

Open a terminal, navigate to the location where you want to set up the project, and run the following command:​

git clone https://github.com/devnur-org/foodie-be

Navigate to the project directory

cd foodie-be

Installing dependencies

Next, install the project’s dependencies by running the following command:

npm install

Create a new branch (Optional)

git checkout -b rest-apis

Excellent! The project is now set up and ready for you to proceed with this tutorial. To achieve the current project stage, follow the steps outlined below:

Creating the GET Request

We’ll begin by adding a JSON response to our root route. We’ll replace the res.send function with a JSON response that includes a message for the client. Let’s see the result when we make the request, as demonstrated below:

server.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.json({
        message: "Hello, REST APIs"
    })
})

app.listen(3000, () => console.log('Server running on port 3000'));

To start the server, open a terminal and run the command npm start. While you could use a browser, we’ll be using a specialized tool called Postman for sending these requests and performing additional tasks. Download and install Postman, then follow the steps below.

To make an API request in Postman, begin by creating a new request. Click the “New” button and choose “HTTP” from the pop-up menu.

A new tab will appear, showing an input field and several options to configure the request. For now, we’ll type http://localhost:3000 into the URL field and click the “Send” button. This will send the request to the server currently running and display the response in the Response section.

In the Response section, as shown in the image above, there are several options to review the output. Typically, developers prefer using the “Pretty” tab when dealing with JSON responses, as it formats the data in a more organized and readable way, especially when responses are large. Now, let’s proceed to create a POST request. The result displayed in the Response section is based on the following code:

server.js
res.json({
    message: "Hello, REST APIs"
})

Let’s include a value, such as the current date, that will update each time a GET request is sent to the server. We can add this to our res.json.

server.js
res.json({
    message: "Hello, REST APIs",
    createdAt: new Date()
})

Stop the currently running server and restart it. After sending the request several times, you’ll notice that the createdAt value updates with each request.

Creating the POST Request

With a GET request, we can’t send data to the server via the API. However, we can use a POST request to do so. To enable POST requests on the index route, we need to configure it in the Express application. Let’s add the POST request to our existing server:

server.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.json({
        message: "Hello, REST APIs",
        createdAt: new Date()
    })
})

app.post('/', (req, res) => {
    const body = req.body;
    res.json({
        status: true,
        message: "Received data from the client",
        data: body
    })
})

app.listen(3000, () => console.log('Server running on port 3000'));

In line 12, we extract the body from the request parameters and return it as a JSON response. Restart the server session, then switch to Postman and create a new request. Use the same URL, but instead of GET, select POST from the dropdown to the left of the URL. Next, click on Body under the URL input field, choose raw from the radio options, and a new dropdown will appear on the right. Ensure it’s set to JSON and then input the following JSON:

{
    "first_name": "John",
    "last_name": "Doe",
    "email": "johndoe@email.com"
}

Let’s submit the request and observe the response we receive.

Uh-oh! We’re not getting the response we expected. The JSON we entered in the body should have been returned under the “data” key, but it didn’t appear. We anticipated this, and here’s why: Express doesn’t automatically parse incoming JSON data from HTTP requests. To fix this, we need to use middleware.

Middleware is a process that runs before a request is handled. It acts as a request handler that can intercept or modify requests before they’re processed. Let’s add the JSON middleware from the Express library to handle this.

server.js
const express = require('express');
const app = express();

app.use(express.json());

app.get('/', (req, res) => {
    res.json({
        message: "Hello, REST APIs",
        createdAt: new Date()
    })
})

app.post('/', (req, res) => {
    const body = req.body;
    res.json({
        status: true,
        message: "Received data from the client",
        data: body
    })
})

app.listen(3000, () => console.log('Server running on port 3000'));

Let’s restart the server and try sending the POST request again

Awesome! Express is now correctly parsing the JSON data sent in the client’s request. Try adding new values to see how the data you send in the body is received by the server.

Creating the PUT Request

The purpose of a PUT request is to update existing data. Unlike POST, which is primarily used to create new data, PUT doesn’t change the server’s state. Let’s implement the PUT request on the index route.

server.js
const express = require('express');
const app = express();

app.use(express.json());

app.get('/', (req, res) => {
    res.json({
        message: "Hello, REST APIs",
        createdAt: new Date()
    })
})

app.post('/', (req, res) => {
    const body = req.body;
    res.json({
        status: true,
        message: "Received data from the client",
        data: body
    })
})

app.put('/:id', (req, res) => {
    const body = req.body;
    const id = req.params.id;
    res.json({
        status: true,
        message: "Updated data on the server",
        data: body,
        id: id
    })
})

app.listen(3000, () => console.log('Server running on port 3000'));

In line 22, we’ve included :id in the path string, which acts as a placeholder in the URL. Imagine a scenario where several users want to update their first or last name—how would the server identify which user is sending the PUT request to update their details? This is where path variables come in. We pass a unique user ID in the URL, which allows us to dynamically handle each request. To access the path variable from the URL, we use req.params and reference the placeholder, such as req.params.id to retrieve the user ID.

Restart the server, open Postman, and create a new request. Change the HTTP verb to PUT and enter http://localhost:3000/:id. In the Params section, you’ll notice the path variables appear, where you can enter a value in the Value field next to the corresponding Key. Let’s input a random username—for this example, I’ll use johndoe.

As you can see, the response body contains the data we sent, along with the URL path variable. Try changing the path variable to something different and observe the result.

Creating the DELETE Request

The DELETE request is a lot similar to PUT request but has a total different purpose. If we want to delete a resource using the backend, we notify the server with a DELETE request. Let’s add the DELETE request in our backend.

server.js
const express = require('express');
const app = express();

app.use(express.json());

app.get('/', (req, res) => {
    res.json({
        message: "Hello, REST APIs",
        createdAt: new Date()
    })
})

app.post('/', (req, res) => {
    const body = req.body;
    res.json({
        status: true,
        message: "Received data from the client",
        data: body
    })
})

app.put('/:id', (req, res) => {
    const body = req.body;
    const id = req.params.id;
    res.json({
        status: true,
        message: "Updated data on the server",
        data: body,
        id: id
    })
})

app.delete('/:id', (req, res) => {
    const id = req.params.id;
    res.json({
        status: true,
        message: `Deleted resource with the id ${req.params.id}`,
    })
})

app.listen(3000, () => console.log('Server running on port 3000'));

Restart the server, open Postman, and create a new request. Change the HTTP verb to DELETE and enter http://localhost:3000/:id. For the Path Variable, let’s use johndoe. When you send the request, the Response body will display a message saying “Deleted resource with the id johndoe”.

Structuring Routes

As we can observe, our server.js file is becoming quite large, and in the future, we may have more routes beyond just the index route, such as users, foods, or categories. To better organize it, let’s create a folder called routes and move the index routes there. Before we do that, we won’t be exporting the app variable for use in the routes because Express provides a Router middleware, which we will use with the app. Let’s take a look at how to implement this.

Let’s start with creating the routes folder

mkdir routes

Now, let’s add the index route file in the routes folder

touch routes/index.js

Before transferring any code from server.js, we need to import the Express library and create a constant to hold the router from Express. Next, we will copy the app.get, app.post, app.put, and app.delete functions from server.js to routes/index.js. We will then refactor the code to replace the app keyword with router, as app will not be available. Finally, we will export the router.

index.js – routes
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
    res.json({
        message: "Hello, REST APIs",
        createdAt: new Date()
    })
})

router.post('/', (req, res) => {
    const body = req.body;
    res.json({
        status: true,
        message: "Received data from the client",
        data: body
    })
})

router.put('/:id', (req, res) => {
    const body = req.body;
    const id = req.params.id;
    res.json({
        status: true,
        message: "Updated data on the server",
        data: body,
        id: id
    })
})

router.delete('/:id', (req, res) => {
    const id = req.params.id;
    res.json({
        status: true,
        message: `Deleted resource with the id ${req.params.id}`,
    })
})

module.exports = router

To set up the index route for our server, we need to add a new middleware using app.use, which takes two parameters: the path (which will be /) and the path to the file. Let’s add this index route configuration to server.js.

server.js
const express = require('express');
const app = express();

// Route Paths
const indexRoute = require('./routes');

app.use(express.json());

// Route middlewares
app.use('/', indexRoute);

app.listen(3000, () => console.log('Server running on port 3000'));

Restart the server if it’s currently running and test all routes (GET, POST, PUT, and DELETE) to ensure each one returns the expected response.

The User Path

For understanding more closely on how these routes work, we are providing with a new path /users which will include logic related to what every HTTP verb we dicsused has. This will be self learning exercise. First we remove all the verbs from the index route except the GET request and we will be returning plain HTML and lastly, we will be providing the file for routes/user.js file for self learning.

index.js. – routes
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
    res.send("<h1>Hello from Node.js and Express</h1>")
})

module.exports = router
users.js – routes
const express = require("express");
const router = express.Router();

const users = [
    {
        id: 'johndoe',
        first_name: "John",
        last_name: "Doe",
        email: "john@doe.com",
    },
    {
        id: 'maryjohn',
        first_name: "Mary",
        last_name: "John",
        email: "mary@doe.com"
    },
    {
        id: 'williamjohn',
        first_name: "William",
        last_name: "John",
        email: "william@doe.com"
    }
]

router.get("/", (req, res) => {
    res.json({
        status: true,
        message: "Retrieved all users",
        data: users
    })
})

router.post("/", (req, res) => {
    const body = req.body;
    users.push(body)
    res.json({
        status: true,
        message: `Added ${body.first_name} ${body.last_name} to users.`,
        data: users
    })
})

router.put("/:id", (req, res) => {
    const id = req.params.id;
    const body = req.body;
    const updatedUsers = users.map(user => {
        if (user.id === id) {
            return { ...user, ...body }
        }
    })
    res.json({
        status: true,
        message: `Updated user with id ${id}`,
        data: updatedUsers.find((user) => user.id === id)
    })
})

router.delete("/:id", (req, res) => {
    const id = req.params.id;
    const updatedUsers = users.filter(user => user.id !== id)
    res.json({
        status: true,
        message: `Deleted user with id ${id}`,
        data: updatedUsers
    })
})

module.exports = router;

Congratulations 🥳 on finishing this tutorial! Explore the routes/user.js file to gain a deeper understanding of how an API interacts with a database. We hope you’ve grasped the fundamentals of REST APIs using Node.js and Express.

Fed up with constantly restarting the server for the changes? Let’s see what tools can we use to bypass this repetitive task. Learn more here


Share with your audience and help them grow too!

About Author

Arslan Mushtaq

A skilled software developer with 10+ years of experience in mobile and web development, proficient in technologies such as React Native, Native Android, and iOS for mobile, and React.js and Next.js for web development. Additional expertise in backend technologies including Node.js, Express.js, Amazon Web Services, and Google Firebase, facilitating collaboration with cross-functional teams across various domains.