Setting up a REST-API using Express

Subscribe to my newsletter and never miss my upcoming articles

In this article, I want to show you a quick and solid way to set up a REST-API with ExpressJS. This does not include any kind of authentication or authorization. We will just be setting up one single route and define some standards that will help us expand this API if we chose to do so.

Prerequisites

Before we can start coding we need to get a version of NodeJS and NPM installed on our system. Just head over to the official website linked here and download the LTS (long time support) version of NodeJS. This will automatically install NPM alongside it as its package manager.

Next, we generate our project by creating a folder called express_api_template and then using npm to initialize a NodeJS project.

$ mkdir express_api_template
$ cd express_api_template/
$ npm init

npm, init will walk you through a process for setting up a new project. I usually use the default settings except for entry point. I like to call my main javascript file server.js instead of the default index.js and then fill out the author. After this is done we need to install ExpressJS by adding it to our package.json. We will use the following command for this.

$ npm install express

After it is finished downloading we should have a node_modules folder and the two files package.json and package-lock.json.

The Basics

First, we need to create two new folders and two new files, and some new dependencies.

$ mkdir src
$ mkdir src/config
$ touch src/server.js src/config/config.env
$ npm install colors dotenv
$ npm install nodemon --save-dev

The difference between the plain install and the --save-dev install is that the plain one installs dependencies needed for production. --save-dev installs dependencies only needed for development. But what did we actually install here?

  • colors: This package is used to make the console outputs colorful.
  • dotenv: This package loads environment variables from .env files into process.env.{variable_name}
  • nodemon: This is used in development for reloading your server every time you save changes.

Installing all this won't make the application run. For that we need to do two more things:

  1. Configuring our package.json to start server.js
  2. Implementing a basic Express server in server.js

Let's start by configuring package.json like this:

{
  "name": "express_api_template",
  "version": "1.0.0",
  "description": "",
  "main": "src/server.js",
  "scripts": {
    "start": "NODE_ENV=production node src/server.js",
    "dev": "NODE_ENV=development nodemon src/server.js"
  },
  "author": "Jakob Klamser",
  "license": "ISC",
  "dependencies": {
    "colors": "^1.4.0",
    "dotenv": "^8.2.0",
    "express": "^4.17.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.4"
  }
}

We defined two commands to use with npm. The first one is for production. It sets the NODE_ENV variable to production and then starts the server by using the node-command. The second one is for development and is setting the NODE_ENV to development and then using nodemon instead of node so that we can make use of the reload-on-save functionality while we are in development.

NOTE: If you are using Windows as an operating system you need to install cross-env as a development dependency to set the NODE_ENV.

$ npm install cross-env --save-dev

And then edit the two scripts like this:

"scripts": {
  "start": "cross-env NODE_ENV=production node src/server.js",
  "dev": "cross-env NODE_ENV=development nodemon src/server.js"
},

For all of this to work, we need to finish up step two first. We have to create an express application and then start it by using a port that we define in our config.env. Add the port to the file like this:

PORT=5000

Now we can go ahead and begin with writing some code on server.js.

const express = require('express');
const dotenv = require('dotenv');
const colors = require('colors');

dotenv.config({ path: 'src/config/config.env' });

const app = express();

const PORT = process.env.PORT || 5001;

app.listen(PORT,
  console.log(`Server up and running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow.bold));

This is really straight forward, we set the path to our config.env and then initialize the express app. After that, we start listening at the port we just set in our config.env. If we run the following command:

$ npm run dev

You should see the following output:

[nodemon] 2.0.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/server.js`
Server up and running in development mode on port 5000

Now we got a really basic setup going. Now let's make this a template for a REST-API in the next section.

Debug and Security

Our goal is to add some more features like logging more information to the console for debugging purposes and securing the API.

To achieve the aforementioned goals we need to add a few more dependencies.

$ npm install helmet cors
$ npm install morgan  --save-dev

What do these three new dependencies do?

  • helmet: This package is a middleware that helps to secure your API by adding multiple HTTP headers.
  • cors: This middleware helps us implementing CORS.
  • morgan: This is a simple HTTP request logger, it outputs the incoming requests to the node console.

After installing all these middlewares we need to go ahead and add them to our express application.

// After the other require statements:
const helmet = require('helmet');
const cors = require('cors');

// Between const app and const PORT:
// Development Setup
if (process.env.NODE_ENV === 'development') {
  // require morgan if in development mode
  // setting morgan to dev: https://www.npmjs.com/package/morgan#dev
  app.use(require('morgan')('dev'));
}

// Put all the server-wide middleware here
app.use(cors({
  origin: process.env.CORS_ORIGIN,
}));
app.use(helmet());

Most notably is the new check on the current NODE_ENV. We do that because we only require morgan if we are in development mode. If you later want to add in something like a data seeding script for a database in development then you can do it right there.

After that new check, we connect the middlewares to our express application. For cors, we configure an origin. This means only requests from this origin are allowed to communicate with our API. For example a frontend application you built. We just need to add the address to our config.env file like this:

CORS_ORIGIN=http://localhost:8000

The port might be different depending on your web applications development set up. If that's the case just go ahead and change it.

Endpoint and Custom Middleware

Now that we are done securing the API we will implement two basic middlewares and one example route. To keep the project clean and maintainable we will add three more folders and four new files.

$ mkdir src/routes src/middleware src/controllers
$ touch src/middleware/notFound.js src/middleware/errorHandler.js src/routes/post.js src/controllers/postsController.js

Middleware

We start by creating our first middleware function in notFound.js that handles requests that don't hit an API endpoint by throwing a 404 Not Found Error.

const notFound = (req, res, next) => {
  const error = new Error(`Not Found - ${req.originalUrl}`);
  res.status(404);
  next(error);
};

module.exports = notFound;

It is simply a function that takes in the request, response, and next. We create an error and set the HTTP status code to 404 and pass in the error to next.

This middleware alone won't help us at all. We need something to handle incoming errors, such as the Not Found Error we just created. For that, we implement our next middleware function called errorHandler.

const errorHandler = (error, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode);
  res.json({
    message: error.message,
    stack: process.env.NODE_ENV === 'production' ? ':(' : error.stack,
  });
};

module.exports = errorHandler;

If someone hits a route that has no endpoint our API will return a JSON object that contains the error message and if we are running in development it also returns the stack.

The last step is to add the middlewares to our server.js.

// After the other require statements:
const notFound = require('./middleware/notFound');
const errorHandler = require('./middleware/errorHandler');
// Custom middleware here
app.use(notFound);
app.use(errorHandler);

Router

We are getting closer to the finish line. There are just two steps left. We are focusing on one of them now: Adding a route. For that, we need to ask ourselves what route we want to add. In this article, I want to add two different GET routes one that gets all posts and one that gets an article by its ID. Let's get started by implementing our route in the file post.js.

const express = require('express');

const router = express.Router();

// Controller Methods
const { getPosts, getPostById } = require('../controllers/postsController');

router.route('/')
  .get(getPosts);

router.route('/:id')
  .get(getPostById);

module.exports = router;

The express router lets us define routes based on HTTP verbs like GET, POST, etc. We just need to add our controller methods, that we will implement later, to the HTTP verb, and the router will do his magic. In server.js we need to add the router like this:

// Between helmet and custom middleware:
// All routes here
app.use('/api/posts', require('./routes/post'));

This will throw an error because we did not implement the controller functions yet.

Controllers

Now we are at the last step for our REST-API template. The controller functions. We will need to create two of them, getPosts and getPostById. Let's get to work by implementing these methods in postsController.js.

const postsArray = [
  {
    id: 1,
    title: 'React from scratch',
    content: 'In this article we will create a ToDo App in React from scratch.... etc.etc.etc.',
    author: 'Jakob Klamser'
  },
  {
    id: 2,
    title: 'Vanilla JavaScript Basics',
    content: 'Today we will discuss some basic JavaScript fundamentals like array manipulation, currying etc.',
    author: 'Jakob Klamser'
  },
  {
    id: 3,
    title: 'VS Code Extensions',
    content: 'I wanted to show you some of my favorite VS Code Extensions.... Bracket Pair Colorizer etc.etc.',
    author: 'Jakob Klamser'
  },
  {
    id: 4,
    title: 'ExpressJS REST API',
    content: 'Is this the current article?',
    author: 'Jakob Klamser'
  },
];


// @route   GET api/posts
// @desc    Get All Posts
// @access  Public
exports.getPosts = (req, res) => {
  const posts = postsArray;
  return res.status(200).json({
    success: true,
    count: posts.length,
    data: posts,
  });
};

// @route   GET api/posts/:id
// @desc    Gets a post by ID
// @access  Private
exports.getPostById = (req, res) => {
  const post = postsArray.filter(post => post.id === Number(req.params.id));
  console.log(post);
  if (post[0] !== undefined) {
    return res.status(200).json({
      success: true,
      data: post[0],
    });
  }
  return res.status(404).json({
    success: false,
    error: 'No post found',
  })
};

At the top of the file, we have some static data. After that, we export two functions. The first one, getPosts, returns the whole list of static data. The second method, getPostById, returns one object from the array if the id matches or it returns an error if no post matches the id provided in the request.

The last thing we need to do is enable JSON for our application by adding another middleware.

// Right below helmet:
app.use(express.json());

Conclusion

You can now type in localhost:5000/api/posts or localhost:5000/api/posts/2 to access the API (while it is running). I hope you enjoyed this quick guide for setting up a template express API. You can build on that by adding a database, authentication and authorization, more endpoints, and so on. Let me know what you think about it and if you build something on top of this template. The whole project can be found on my GitHub.

Photo by Brian McGowan on Unsplash

No Comments Yet