How to create a simple HTTP API with authentication with Node.js?

In this example we’ll see how to create an HTTP API with 3 main components: Signup, Login and a login-only route. Let’s start by setting up a new project and including Express for handling the routes, JSON Web Token to handle auto-expiring tokens, and bcrypt to encrypt user passwords. Initialize the new project in an empty folder. We’ll name the main file index.mjs to utilize top level awaits and have import/export functionality.

npm init -y
npm i --save express jsonwebtoken bcrypt

Let’s start by setting up the Express router and running the server. We will be using JSON all around our API, and we will prefix all routes with /api/.

const port = 8000;
const app = express();
const router = express.Router();

app.use(express.json());
app.use('/api/', router);

app.listen(port, () => {
  console.log(`Server started at port ${port}`)
});

Setting up the Signup, bcrypt-ing the password and issuing a token

Bcrypt needs to know the number of rounds to do, jsonwebtoken will use a secret to validate tokens, and for users collection for the sake of simplicity we will just use a map.

const rounds = 10;
const secret = 'usethissecretfortokens';
const Users = {};

The token generation function is very simple and it has tons of different options, but we will use the easiest synchronous version. As a payload, we will pass the user object during the Signup and Login:

const generateToken = (payload) => {
  return jwt.sign({ data: payload }, secret, { expiresIn: '24h' });
};

Now on to the actual signup. We won’t do any special fields validation or email verifications, we’ll keep it simple, remember?

router.post('/signup', (req, res) => {
  const email = req.body.email;
  const pass = req.body.password;
  if (!email) {
    return res.status(400).json({ error: 'Email is required' });
  }
  if (!pass) {
    return res.status(400).json({ error: 'Password is required' });
  }
  if (Users[email]) {
    return res.status(409).json({ error: 'User already exists' });
  }
  bcrypt.hash(pass, rounds, (err, password) => {
    if (err) {
      return res.status(500).json(err);
    }
    Users[email] = { email, password };
    res.status(200).json({ token: generateToken(Users[email]) });
  });
});

What it does is it simply checks for the fields existence, checks for email to be unique (again, no real validations of the field values, we are simplifying it), then runs the bcrypt hashing. Hashing is a rather expensive operation, so it’s asynchronous. Then, we are taking the resulting hash and storing it in the User object. When we generate the token, we pass the whole user object to it, so when the token is going to be validated, it will know the context.

To test the endpoint, we will need to send a JSON document with email and password to /api/signup. The response should be a JSON document with a single token field. The sample requests for Postman are attached at the end of the post.

Now, on to the simple login endpoint which will validate bcrypted password and issue new tokens.

router.post('/login', (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  const user = Users[email];
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  bcrypt.compare(password, user.password, function(err, match) {
    if (err || !match) {
      return res.status(401).json({ error: 'Authentication failed' });
    }
    res.status(200).json({ token: generateToken(user) });
  });
});

And now the last, most important part – we will create a middleware that does a token validation, assigns a proper context to the request, and runs the next handler. Tokens will need to be sent in the Authorization header.

const verifyToken = (token, cb) => {
  if (!token) {
    return cb({ error: 'Authorization token is empty' }, null);
  }
  jwt.verify(token, secret, cb);
};

const tokenMiddleware = (req, res, next) => {
  const token = req.headers.authorization;
  verifyToken(token, (err, value) => {
    if (err) {
      return res.status(500).json({ error: 'Token verification failed'});
    }
    console.log(`${value.data.email} token ${token} verified`);
    req.user = value.data;
    next();
  });
};

This middleware can now be used together with any route to make it guarded with tokens, i.e.

router.post('/posts/create', tokenMiddleware, (req, res) => {
  // do something to create a new Post
  res.status(200).json({ result: 'Post created successfully' });
});

And this is how our whole code looks like right now.

import express from 'express'
import jwt from 'jsonwebtoken'
import bcrypt from 'bcrypt'

const port = 8000;
const rounds = 10;
const secret = 'usethissecretfortokens';
const app = express();
const router = express.Router();
const Users = {};

const generateToken = (payload) => {
  return jwt.sign({ data: payload }, secret, { expiresIn: '24h' });
};

const verifyToken = (token, cb) => {
  if (!token) {
    return cb({ error: 'Authorization token is empty' }, null);
  }
  jwt.verify(token, secret, cb);
};

const tokenMiddleware = (req, res, next) => {
  const token = req.headers.authorization;
  verifyToken(token, (err, value) => {
    if (err) {
      return res.status(500).json({ error: 'Token verification failed'});
    }
    console.log(`${value.data.email} token ${token} verified`);
    req.user = value.data;
    next();
  });
};

router.post('/login', (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  const user = Users[email];
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  bcrypt.compare(password, user.password, function(err, match) {
    if (err || !match) {
      return res.status(401).json({ error: 'Authentication failed' });
    }
    res.status(200).json({ token: generateToken(user) });
  });
});

router.post('/signup', (req, res) => {
  const email = req.body.email;
  const pass = req.body.password;
  if (!email) {
    return res.status(400).json({ error: 'Email is required' });
  }
  if (!pass) {
    return res.status(400).json({ error: 'Password is required' });
  }
  if (Users[email]) {
    return res.status(409).json({ error: 'User already exists' });
  }
  bcrypt.hash(pass, rounds, (err, password) => {
    if (err) {
      return res.status(500).json(err);
    }
    Users[email] = { email, password };
    res.status(200).json({ token: generateToken(Users[email]) });
  });
});

router.post('/posts/create', tokenMiddleware, (req, res) => {
  // do something to create a new Post
  res.status(200).json({ result: 'Post created successfully' });
});

app.use(express.json());
app.use('/api/', router);

app.listen(port, () => {
  console.log(`Server started at port ${port}`)
});

Here are the sample Postman requests to test the API:

Leave a Comment

Your email address will not be published.