Creating a RESTful API Server

A common design pattern for constructing web apps is called MVC (model, view, control).  In MVC, the app’s code is organized into three separate parts:

    • Code that Models entities and stores instances of the models in persistent storage,
    • Code establishing Views (the GUI of the app) that users interact with, and
    • Code that Controls how the user interacts with the data and the views.

So far, the app that we’ve developed allows users to navigate to different pages (index, login, main, etc.) via endpoints. The templates and partials are code that establishes the views of the app.  The express routers control what the user sees and how the user interacts with the app.  We are now at a point where we need to define the entities that we wish to maintain in persistent storage (e.g. Users). In the MVC paradigm, the definitions for these entities are called models.

We will utilize a MonogoDB database and will develop routers that will allow any client to perform CRUD (Create, Read, Update, and Delete) operations on the data in the database. The endpoints that we create for this data server will form the web service’s application programming interface (API).  The API informs client developers which entities are stored on the server, the type of CRUD operations that can be performed on the entities, the data required by the server to perform the operation, and what the client can expect back from the server.

Web services often use a REST architecture.  REST stands for REpresentational State Transfer and simply describes the fact that on two systems, the same set of data can be represented differently, and in order to transfer that data (that is in a particular state) from one system to another, the two systems have to agree on how to represent the data when the data is being transferred between the two systems and must agree on the transfer (communication) protocol used to transmit the data between the two systems.

For example, a user named Mike Tyson on a server may be represented by a document in a collection named User in a MongoDB database.  Mike may also be represented by a Profile web page written in HTML in a client.  They both contain the same data (e.g. name, email address, phone number), but that data is represented differently in the two systems.  Now at any point in time, if the client wants to perform a CRUD operation on the data in the server, the two systems must agree on how to exchange the data.  Web services today most often transfer JSON strings over HTTP.

In order to make a web service efficient and scalable, developers often utilize RESTful web API design principals.  Two main RESTful design principals are Platform Independence and Service Evolution.  Platform Independence requires that any client, written in any programming language, running on any hardware, should be able to utilize the web service’s API, so long as they can communicate using the specified transfer protocol.  Service Evolution requires that the continued development of the API be independent of the development of clients, and that all modifications to the API be backwards compatible.

To keep with the Platform Independence and Service Evolution design principals we are going to keep our API service separate from the client that we have been developing.  Our API will be served by a new and separate node app.  We will establish here that state information will be transmitted between clients and our server as JSON strings via HTTP.

Other RESTful design principals include:

    • REST APIs are designed around resources that can be accessed by the client.
    • A resource has a URI (endpoint) that uniquely identifies the resource.
    • REST APIs use a uniform interface. We’ll use the standard HTTP methods (GET, POST, PATCH, and DELETE).
    • REST APIs use a stateless request model.  This implies that each request is independent and the server does not maintain transient state information between requests.

To begin, our API server will provide clients the ability to store and retrieve user profile information via the /users endpoint.  The semantics for the various uses of the /users endpoint are below.

POST /users         // create a new user and generate an authentication token 
POST /users/login   // validate a user's credentials, generate an auth token 
GET /users/me       // get user's account information 
PATCH /users/me     // modify a user's account information 
DELETE /users/me    // delete a user's account 
POST /users/logout  // log a user out

In this chapter, we’ll start the development of the API be creating the POST /users endpoint to allow users to create accounts.

Let’s get started.

Create a New VSC Project

Create a new directory on your laptop for a new VSC project.  The project will contain the code for a new Node.js server that will serve up your app’s REST API.  Throughout this tutorial I will refer to the directory that you created as api-dir.

Create the following directories and files in your project (file names are in blue).

api-dir/.env
api-dir/src
api-dir/src/app.js
api-dir/src/db
api-dir/src/db/mongoose.js
api-dir/src/models
api-dir/src/models/user.js           // note that user is singular
api-dir/src/routers
api-dir/src/routers/users.js         // note that users is plural

Set Up Your Project

Perform the following steps for your new project.

    1. Initialize npm.
    2. Configure the main and the script properties in package.json.  Assume the app’s main script will be located at src/app.js.
    3. Install the express npm module.
    4. Install the nodemon npm module as a developer dependency.
    5. Initialize git.
    6. Create a .gitignore file and add both node_modules/ and .env to it.
    7. Create a new heroku project using the heroku CLI.

Load Additional NPM Modules

Mongoose is a JavaScript library written for Node.js that will allow us to create Schema for the entities that we wish to store in the MongoDB database.  The library also provides methods for saving instances of the entities in the form of documents into the database and much more.

The env-cmd is a nice npm utility module that simply creates a new environmental variable for each key=value pair found in the file named .env.

Please install both of these modules.

$ npm install mongoose
$ npm install env-cmd

Set Up env-cmd

Since we’ll be running our web app API on both localhost (to test) and on heroku, we need to establish a way for the app to determine which MongoDB URL to use.  One way to do this is to set an environmental variable that our node app will reference.

When we are developing on localhost we can use env-cmd to create environmental variables for each of the key=value pairs in our .env file.  To start, add the following 2 lines to your .env file.  The first line defines the PORT environmental variable and the second defines the MONGODB_URL environmental variable.

PORT=3001
MONGODB_URL=mongodb://127.0.0.1:27017/web-app-api

Note that web-app-api will be the name of our MongoDB database.

Recall that the start script in package.json is used by heroku to start up the application and the dev script is used by us, the developer.  Since we only want these environmental variables set when we are running the app in developer mode, we can run env-cmd as part of the dev script. Open package.json and change the dev script to read as follows.

"dev": "env-cmd nodemon ./src/app.js"

Now when we type “npm run dev” env-cmd will run first, setting the two environmental variables defined in .env. Then nodemon will start our app.

Set MONGODB_URL For Heroku

The heroku CLI allows developers to establish environmental (configuration) variables, much like env-cmd does.   To do so, we use the heroku config command in the terminal.

Since heroku sets the PORT environmental variable automatically, we don’t need to set it with heroku config, but we do need to set MONGODB_URL.

Navigate to your Database Deployments page on cloud.mongodb.com and click on the Connect button.  On the next page, you should see three choices under Choose a connection method.  Click on Connect your application. On the following page, make sure that Node.js is chosen as the Driver and the Version is 4.0 or later, then copy the connection string to your clipboard.

Now back in the VSC terminal, let’s configure the MONGODB_URL heroku environmental variable.  As we set this up, please don’t press enter until instructed to do so.

Enter the following command in the terminal while replacing <db-connection-string> with your Altas database connection string.  Also, if you have a Windows machine, replace the single quotes with double quotes.

$ heroku config:set MONGODB_URL='<db-connection-string>'

Before you press enter, you need to make two changes to your database connection string.  First, replace <password> with your Atlas database user’s password.  Second, replace myFirstDatabase with web-app-api.

Now press enter.

Verify the environmental variable is set correctly using the following command.

$ heroku config

If you need to delete it and try again, you can delete the variable using the following command.

$ heroku config:unset MONGODB_URL

Connect to the MongoDB Database

Ok, now we finally get to code!

I mentioned earlier that the mongoose module was written specifically to help node applications perform MongoDB operations.   We’re going to be using mongoose alot, so take a look at their documentation which can be found at https://mongoosejs.com/docs/guide.html.

First, we need to connect mongoose to the MongoDB database.   This will allow  mongoose to interact with our database.  To do so, add the following code to src/db/mongoose.js then read the description below.

const mongoose = require('mongoose')
console.log(`Connecting to ${process.env.MONGODB_URL}`)
mongoose.connect(process.env.MONGODB_URL).catch((e) => {
  console.log(e)
}

Line 1 loads the mongoose module.

Regardless of whether we are in developer mode or if the app is on heroku, the URL to the MongoDB database is stored in the MONGODB_URL environmental variable.  Line 2, simply prints the value to the console.

Line 3 calls mongoose.connect(), passing to it the url of the MongoDB database.  If mongoose is unable to connect to the database, the connect() method returns a rejected Promise, on which we call catch() to print the error that was thrown.

Notice that we aren’t exporting anything. The reason for this is that we are going to simply run the code in mongoose.js from the src/app.js.

Define the User Model

Our goal is to store data about users (email, password, and username) in the MongoDB database.  The specifications (names, types, default values, etc)  about the data to be stored is defined as a JavaScript object in a mongoose Schema.  Each schema is mapped to a MongoDB collection.  The user schema will establish the users collection in the database.

The Schema we specify for the user entity will be passed to the mongoose model() function which will return a new User model. The User model will serve as a constructor for instances of users.   An instance of the User model will be stored as a document in the users collection in the database.

Note that when mongoose needs to store or retrieve a document in the database, it automatically looks for a collection whose name is the plural lowercase version of the model name. So, for example, when we create an instance of User and save it to the database, mongoose will save it in a collection named users.  Interestingly, I tested this out by creating a model named Fox and saved an instance of it in the database. Guess what the name of the collection it created was.  The answer is at the bottom of this chapter.

The code below creates the User model and exports it.  We will create instances of the User model and save them to the database later when we create the API /users router.

Copy the code below into src/model/user.js and then read the description of the code below.

const mongoose = require('mongoose') 

const Schema = mongoose.Schema

const userSchema = new Schema({ 
    email: {
      type: String,
      unique: true,
      required: true,
      trim: true,
      lowercase: true
    },
    password: {
        type: String,
        required: true,
        trim: true,
        minLength: 8
      },
      name: { 
        type: String,
        unique: true,
        required: true,
        trim: true
      }
  })

  const User = mongoose.model('User', userSchema);

  module.exports = User

Explanation of the Code

Line 1 loads the mongoose module.

Line 2 sets the constant named Schema to the mongoose.Schema constructor.

Lines 5-24 creates a schema for the user entity.  The schema is created by passing an object to the Schema constructor.  The object describes the data that will be stored in the collection.

The userSchema specifies that documents in the users collection will store a users’s email, password, and name.  Each of these properties (email, password, and name) are mapped to a SchemaType object.  A SchemaType object is a configuration object for an individual property and must, at the very least, specify a data type, but can also specify SchemaType Options.

For example the object defined on lines 6-12 specify that a value for the email property must be

    • a string,
    • unique among all the documents in the collection,
    • required in all documents
    • will be trimmed of leading and trailing whitespace, and
    • will be converted to lowercase before being inserting into the database.

We’ll add more to the user schema later.

One line 16 we create a User model by passing the name of the model ('User') and the user schema to mongoose.model().

Create the /users Router

Our /users router will ultimately allow clients to create, get, modify, and delete documents in the users collection.  For now, we’ll just create a single POST method that will allow users to store new documents in the users collection.

Please copy the code below into your src/routers/users.js file and read the explanation below.

const express = require('express')
const User = require('../models/user')

const router = new express.Router()

// Add a new user
router.post('/users', async (req, res) => {
  const user = new User(req.body)

  try {
    await user.save()
    res.status(201).send(user)
  } 
  catch(error) {
    res.status(400).send(error)
  }
})

module.exports = router

Explanation of the Code

On line 1 we import the express module and on line 2 we import the User model.  Recall that the User model is a constructor.

On line 4 we creates a new express router and on lines 7-17 we create an async callback function for requests that come to the /users endpoint with a POST HTTP method.

In a later chapter we’ll see how a client can send an object in the body of a request using the POST method.  For example, a client might send the following object in the request.

{
    "email": "bob@example.com",
    "password": "abcd1234",
    "name": "Bobo",
}

In our router, we assume that he client has sent an object, like the one above, in the body of the request.  On line 8 we call the User constructor/Model, passing to it the object in req.body, to create a user document.

On lines 12-15 we try to save the user document to the database and if successful, we return a response to the client with a status code 201 (Created) and with the user object in the body of the response.

On lines 14-16 we handle the case that the document was not saved to the database.  In this case we return a response with a 400 status code (Bad Request) along with the error returned in the rejected Promise from save().

Alas, we export the router on line 21.

Create the Driver

We’re almost finished setting up our API server.  Below is the code for src/app.js.  Copy the code below and review the explanation.

const express = require('express')
require('./db/mongoose')

const userRouter = require('./routers/users')

const app = express()
app.use(express.json())

app.use(userRouter)

const port = process.env.PORT

app.listen(port, () => {
    console.log('API service is up on port ' + port)
})

Explanation of the Code

On line 1 we load the express module and on line 2 we run the code in mongoose.js which, if you remember, connects mongoose to the MongoDB database.

Line 4 imports the user router and on line 6, we create an express app.

Line 7 is new.  This call, tells express to convert the bodies of incoming requests from JSON strings to objects.  This lets us avoid having to do it manually in our routers.

On line 9 we register the router for the /users endpoint.

Line 11 creates a constant that is initialized to the value of the PORT environmental variable.

And on lines 13-15 we tell node to listen for incoming requests on the specified port.

Test the API

Since we haven’t written the client side code yet, we need an alternative way to test the API server.  One way is to use Postman. Postman can send requests to any web server and displays the responses that are sent back to Postman.  We can also use MongoDB Compass to view the contents of the database after we’ve created users using Postman.

Rather than include the instructions on how to test your API server with Postman here, I’ve put them in the next chapter.  So, when you’re ready, continue on to the next chapter:  Testing an API Server with Postman.

Answer: Mongoose created a collection named foxes.

© 2022 – 2024, Eric McGregor. All rights reserved. Unauthorized use, distribution and/or duplication of this material without express and written permission from the author is strictly prohibited.