How To create a CRUD API, using Node.js and Express, on Ubuntu 18.04

Featured image

Introduction

An API (Application Programming Interface) is a way of interacting with a service, through a series of predefined requests.

Express is an open source web framework, for Node.js, designed to make developing websites, web apps, and API’s easier.

In this tutorial, you will create a simple CRUD API on a single Ubuntu 18.04 server. The API will be accessible through a public IP address, allowing you to access it from anywhere with an internet connection.

To get started, sign-up for Digital Ocean using this referral link and get $100 credit!

Prerequisites

Step 1 — Create a simple Express App that serves “Hello World”

In this step, we’ll create a very simple API, with one endpoint, that we’ll then build on in future steps.

To begin, create a new directory in a location of your choosing, and create a default package.json file, by running the following command in a terminal:

npm init -y

In the same terminal, install the express dependency:

npm i express

Next, open the project in a code editor of your choice and create a new server.js file.

Within the new file, add the following code:

const express = require('express')
const app = express()
const port = 3000

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

app.listen(port, () => console.log(`App listening on port ${port}!`))

Using the express application, we define a /hello endpoint, that will return the text Hello World!, and run our application on port 3000.

Note that the /hello endpoint will only match for GET requests, as we’ve defined it using the app.get method.

In your terminal, run the following command to start the application:

node index.js

You should see the following output as a result:

$ node index.js
App listening on port 3000!

Now open your favourite browser, and navigate to localhost:3000/hello. If everything worked successfully, Hello World! should be displayed in your browser.

Congratulations - you have created an API, and you can communicate with it via the /hello endpoint. We’ll build on this structure in the following steps, and make our API even better!

Step 2 — Extend the application to get a property from in-memory state.

At this point we have one endpoint, /hello, that returns the text Hello World!. While it’s awesome that we’ve created an API, as far as functionality goes it’s not that helpful.

In this step we’ll take our API and have it return some useful data. We’ll be using an example of employee details.

First, let’s add another endpoint to our application that will retrieve a record.

const express = require('express')
const app = express()
const port = 3000

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

app.get('/employee/:id', (req, res) => {
  console.log(req.params.id)
  res.sendStatus(200)
})

app.listen(port, () => console.log(`App listening on port ${port}!`))

We define a new /employee endpoint, just like we did for the /hello endpoint, but we’re also using a query parameter.

Query parameters are defined using a colon, and the preceding text is used to reference that parameter. In our /employee endpoint, we’ve defined a query parameter called id, and our console.log statement shows how we reference the value.

Stop and start the application, so the new code changes take effect, open your browser and navigate to http://localhost:3000/employee/wiggly.

You should see the following output in your terminal, as a result:

$ node index.js
App listening on port 3000!
wiggly

Notice how wiggly was printed to the terminal, which is what we used in the URL (/employee/wiggly). Change wiggly to anything you like, and you should see that printed in the terminal.

The power of this technique enables us to define one route, which can be used for many different scenarios. For example, an endpoint that can retrieve an employees details using a unique identifier, which is exactly what we’re going to do now!

Let’s create an Object, to act as in-memory state for our application, and add some fictional employees:

/**  
We'll use the Object key as the unique identifier, made up of the
first letter of the employees first name and whole of their last name.
*/
const employees = {
  'sbrown': {
    firstName: 'Steve',
    lastName: 'Brown',
    department: 'Engineering'      
  },
  'jsmith': {
    firstName: 'Janine',
    lastName: 'Smith',
    department: 'Marketing'      
  },
  'kjones': {
    firstName: 'Karen',
    lastName: 'Jones',
    department: 'Sales'      
  },
  'bwilliams': {
    firstName: 'Ben',
    lastName: 'Williams',
    department: 'Administration'
  }
}

Now, instead of logging out the id parameter value, let’s return the employee details from our Object:

app.get('/employee/:id', (req, res) => {
  res.json(employees[req.params.id])
})

As we’ll be returning a JSON object we’re using the res.json method which, among other things, sets all of the correct headers and returns the response in JSON.

Restart the application and navigate to http://localhost:3000/employee/kjones in your browser.

You should see the following response in your browser:

{"firstName":"Karen","lastName":"Jones","department":"Sales"}

Try changing kjones to any of the other unique identifiers, in the employees object, and see the returned values change.

Now, try changing the id to something non-existent (e.g. http://localhost:3000/employee/wiggly) in the browser. Nothing is returned, which is correct, but we’re not giving the user any indication why; did something go wrong, or does the employee just not exist?

Let’s update the endpoint to check if the given employee id exists and, if not, we’ll return a 404 Not Found response, otherwise we’ll return the employee details.

app.get('/employee/:id', (req, res) => {
  const employee = employees[req.params.id]

  if (!employee) {
    return res.sendStatus(404)
  }

  res.json(employee)
})

Restart your application, and try the same URL in the browser. The response should now be Not Found, which is a lot more informative to the user.

In this step, we took a very simple and not very helpful API and transformed it into one that queries data using query parameters. We also learnt how to handle situations where a non-existent employee is queried.

Step 2 — Extend the application to add/delete a property from in-memory state.

We now have an API that can retrieve an employee, based on the employee id, but what if an employee joins/leaves the company? We need a way to add/remove them from our employees list, which is what we’ll achieve in this step.

Let’s focus on people joining the company first. When someone joins, we want to add them to our employees object so they can be queried later on.

To do this we will use a POST request, which we declare in the same way as our GET requests above.

app.post('/employee', (req, res) => {
  res.sendStatus(200)
})

Restart your application to apply our code changes.

Our new route doesn’t do anything, other than return a 200 OK status because, before we get into writing the logic, I want to talk a bit about how we can make a POST request.

When you enter an address into a browser, like we did in the previous steps ( e.g. http://localhost:3000/hello ), we are requesting a resource by making a GET request.

We can’t use the same mechanism to make any other type of request (POST, PUT, DELETE etc), so how do we reach these endpoints? There are quite a few different methods, but we’re only going to focus on one - CURL.

To make a POST request, to our new endpoint, using CURL, run the following command in a terminal:

curl -X POST 'http://localhost:3000/employee'

The only functionality we added to our new endpoint was to return a 200 OK status, which is what you should see as a response in your terminal.

$ curl -X POST 'http://localhost:3000/employee'
OK

Let’s build this logic out to add an employee to our employees object.

The first thing we need to do is to get the new employees details. If we look at our existing employees, we need three pieces of information - firstName, lastName and department.

In the last section we used the req.params property to extract the id URL parameter. When dealing with a POST request, we have the ability to use a request body.

Using CURL, we can combine the Header (-H) flag to specify a content type, which informs the server what format the request content is in, and the Data (--data) flag to pass through a JSON object.

We’ll sending a JSON object so we’ll set a Content-Type header or application/json, to tell the server we’re sending JSON, and we’ll specify our new employee as data:

CURL -X POST 'http://localhost:3000/employee' -H 'content-type: application/json' --data '{"firstName": "John", "lastName": "Doe", "department": "engineering"}'

We are now making a POST request, to http://localhost:3000/employee, with the above JSON object as our Request Body.

By default, our Express application won’t parse this JSON object, so we need to enable some middleware to enable it.

const express = require('express')
const app = express()
const port = 3000

/**
-- Employee object
*/

/** 
Middleware that looks at requests with an application/json 
Content-Type header and stores the request body, as JSON,
in req.body
*/
app.use(express.json())

/**
-- Other endpoints
*/

app.post('/employee', (req, res) => {
  console.log(req.body)
  res.sendStatus(200)
})

Before defining any of your routes, tell your app to use the express.json middleware. Then, in our new endpoint, log the Request Body.

Restart the application and make the request, with the JSON request body, using CURL. You should see the following output in the terminal running your application:

$ node index.js
App listening on port 3000!
{ firstName: 'John', lastName: 'Doe', department: 'engineering' }

We can now receive a new employee object from our new POST endpoint, so let’s build out our logic to ensure we get all three required fields (firstName, lastName and department), construct the id from the firstName and lastName, and then add it to our employee object if that id isn’t already in use.

app.post('/employee', (req, res) => {
  const { firstName, lastName, department } = req.body

  if (!firstName || !lastName || !department) {
    // 400 = bad request. It indicates to the user that
    // there was something wrong with their request.
    return res.status(400).send('One or more required fields are missing')
  }

  const id = (firstName[0] + lastName).toLowerCase()

  if (employees[id]) {
    // Provide a custom message so the user knows what the
    // problem with the request is.
    return res.status(400).send('A user with that id already exists')
  }

  // We set the employee properties explicitly, just in case
  // the user sends other fields through that we're not interested
  // in storing.
  employees[id] = { firstName, lastName, department }

  res.sendStatus(200)
})

Restart the application and try the following scenarios:

Now, let’s use what we’ve learnt so far to create a new DELETE endpoint that takes an employees id and removes it from our employees object.

app.delete('/employee/:id', (req, res) => {
  const employee = employees[req.params.id]

  if (!employee) {
    return res.sendStatus(404)
  }

  delete employees[req.params.id];

  res.sendStatus(200)
})

You’ll notice this is very similar to our GET employee endpoint; we’re using the URL parameter, checking that the employee exists and returning a 200 status if everything succeeds. The only difference is we delete the employee, from the employees object, by their id.

Making a DELETE request in CURL is very similar to the POST request we saw above:

curl -X DELETE 'http://localhost:3000/employee/sbrown'

Restart the application and try the following:

Congratulations - you’ve created a CRUD API!

In this step we really took our API to the next level, by allowing our users to add and delete employees, and we learnt how to deal with POST data by enabling the express.json middleware.

Step 3 — Deploy the API to Digital Ocean

We now have an API that can create, delete and retrieve an employee. What we’ve done so far is awesome, but it only lives on our laptop. To take our application to the next level, we need to host it on a publicly accessible server that we can access it from anywhere in the world.

As per our prerequisites, you should have a Digital Ocean server. First, make a new directory on your server. If you don’t have one, use this referral link to get $100 Digital Ocean credit.

$ mkdir ~/simple-crud

Next, using a terminal in your local project directory, copy the necessary files to your server by running the following command:

$ scp package-lock.json package.json server.js ubuntu@YOUR_IP:./simple-crud/

Replace ubuntu with the user you created, and YOUR_IP` with your servers public IP address.

We’ve now copied our package-lock.json, package.json and server.js file to our new simple-crud folder on our server.

Next, while in the simple-crud folder on your server, install the applications dependencies:

$ npm i

Lastly, start the application using PM2:

$ pm2 start server.js --name "simple-crud"

To make sure the application has started correctly, you can run pm2 logs and ensure you see the following log line:

$ pm2 logs
0|simple-c | App listening on port 3000!

Next, we’ll amend the default NGINX server block to forward all requests to our application.

Use vi to edit the default configuration:

$ sudo vi /etc/nginx/sites-available/default

Then, replace the root location (location /) with the following code, which will forward any HTTP requests to our application.

location / {
	proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $
}

In this step we took our application from only running on our laptop, to running on a Digital Ocean server, meaning our application can be accessed from anywhere in the world!

Step 4 — Use our new API, hosted on Digital Ocean, instead of localhost

We now have an application running on a Digital Ocean server, that can be accessed from anywhere in the world. We know how to access our API locally, but how do we interact with it now?

We just replace localhost with our public IP address in our CURL commands:

$ curl -X GET 'http://157.245.243.1/employee/sbrown'
{"firstName":"Steve","lastName":"Brown","department":"Engineering"}

$ curl -X POST 'http://157.245.243.1/employee' -H 'content-type: application/json' --data '{"firstName": "John", "lastName": "Doe", "department": "engineering"}'
OK

$ curl -X DELETE 'http://157.245.243.1/employee/jsmith'
OK

Conclusion

You have successfully created a CRUD API, with in-memory state, and you deployed it to a Digital Ocean server meaning you can interact with it from anywhere using a public IP address.