Build an authenticated, dockerised API with KoaJS and MongoDB



Photo by Ales Nesetril on Unsplash

A quick guide to creating an authenticated dockerised API with Koa. This can be used as a base to create any API you fancy, from weather apps to restaurant management. It can be easily connected to any frontend, to create a functional web application. Whilst not much functionality will be added in this guide, it will complete all of the setup needed to kickstart your application! For the complete boilerplate, check out this GitHub repo: https://github.com/oflint-1/koa-auth-boilerplate.

Prerequisites:

Technologies used:

Getting started

Firstly, create a new folder and initialise npm

mkdir koa-api && cd koa-api

npm init -y

This will create a basic package.json file and setup your node environment.

Next, install the initial packages for the basic Koa setup.

npm i koa koa-bodyparser koa-router @koa/cors koa-session

Now we can start creating the foundations of our application. Create a new file called app.js

/* app.js */

// Imports
const Koa = require("koa");
const Router = require("koa-router"); // Import routing
const bodyParser = require("koa-bodyparser"); // Imports request parser
const cors = require("@koa/cors");
const session = require("koa-session"); // Import authentication sessions

// Create a new Koa app
const app = new Koa();

// Setup default configuration
app.use(cors({ credentials: true }));
app.keys = ["super-secret-key"]; // Sets application key (Make secure for production)
app.use(session(app)); // Tells app to use sessions

// Initialise request parser
app.use(bodyParser());

// Listen on port 3000
app.listen(3000);

This will create the basic infrastructure for our project. Run node app.js and navigate to localhost:3000 in your preferred web browser and you should see that your web server is running. Currently this should display a Not found message as we haven’t added any routes to our application yet.

Setup basic routes

Now, we want to create our basic API routes.

Create a routes/index.js file

/* routes/index.js */

// Setup basic routes
module.exports = (router) => {
  // Set prefix for all routes
  router.prefix("/api");
  // Include api routes
  router.use("", require("./api"));
};

This will setup the basic router. Now we need to setup the API routes.

Next, create the routes/api.js file.

/* routes/api.js */

// Import and create router
const Router = require("koa-router");
const router = new Router();

// GET basic route
router.get("/", (ctx, next) => {
  ctx.body = "Hello Api!";
});

// Export router
module.exports = router.routes();

This will setup an initial route and add it to the router.

Next, add to app.js after the body parser.

/* app.js */
...
// Add routes
const router = new Router(); // Create new router
require("./routes")(router); // Require external routes and pass in the router
app.use(router.routes()); // Use all routes
app.use(router.allowedMethods()); // Setup allowed methods
...

Now we can see our basic route in action! Once again, run node app.js and visit localhost:3000/api/ and you should see a message saying Hello Api!

Setting up database with docker

Next we’re going to connect our API with a database service. We’re going to dockerise our application and connect it to mongoDB. To start with, make sure that docker is installed and running on your system. See: https://docs.docker.com/engine/install/

Create a new file in the root folder of your app called dockerfile

FROM node

WORKDIR /app

COPY package.json /app

COPY package-lock.json /app

RUN npm install

RUN npm install -g nodemon

COPY . /app

EXPOSE 3000

CMD ["nodemon", "app.js"]

This will create a new node environment, copy over the package files over and install the required packages. It then exposes the correct port and runs nodemon for a server that reloads when any changes are made.

Now we’re going to use docker compose to connect different parts of our API.

Create a docker-compose.yml file, again in the root folder:

#docker-compose.yml

version: "2.0"

# Define the services/containers to be run
services:
  koa-auth-api: #name of your service
    build: . # specify the directory of the Dockerfile
    restart: always
    ports:
      - "3000:3000" #specify ports forwarding
    links:
      - database # link this service to the database service
    volumes:
      - .:/app
    depends_on:
      - database
  database: # name of the service
    image: mongo # specify image to build container from
    ports:
      - "27017:27017"
    volumes:
      - /data/db

This will create the database and the API container, and link them together. To see this in action, quit your node app if it is still running with CTRL + C and run docker-compose up. It will take a few moments to install and create your docker containers but then you should be able to view the application the same as before.

Now we will connect to the database from our API. First, install mongoose with npm i mongoose to connect to the Mongo database.

Now back in app.js, import mongoose with the other imports.

/* app.js */
...
const mongoose = require("mongoose"); // Imports Mongoose which is used to link to MongoDB
...

Now we need to connect to the database. Place this code just before we add the body-parser to our application.

/* app.js */
...
// Connect to database
mongoose.connect(`mongodb://database:27017/test`, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}); // Connects to database
var db = mongoose.connection; // Stores connection
db.on("error", console.error.bind(console, "connection error:")); // Logs any errors
...

Now we have our database connection, we need to use Mongoose to create a model for our user. Create a new file models/user.js.

/* models/user.js */

// Imports
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const userSchema = new Schema(
  {
    // Username - must be unique and fit criteria
    username: {
      type: String,
      required: true,
      unique: true,
      minlength: 1,
      maxlength: 20,
    },
    // Password - will be hashed before storage
    password: { type: String, required: true },
  },
  {
    timestamps: true,
  }
);

// Export model
module.exports = mongoose.model("User", userSchema);

This creates our user model, which will hold all of our users in the database. This will be used later for authentication.

Speaking of authentication…

Setting up authentication

Now we’re ready to setup the final authentication. Firstly, install the requirements.

npm i bcryptjs koa-passport@4 passport-local

Note: We have to use koa-passport 4 due to an issue with passport 6. See: https://github.com/jaredhanson/passport/issues/904.

Create an auth.js file in the root application folder to handle user authentication.

/* auth.js */

// Imports
const passport = require("koa-passport");
const LocalStrategy = require("passport-local").Strategy; // Import strategy
const User = require("./models/user"); // Get user model
const bcrypt = require("bcryptjs"); // Used to encrypt passwords

const options = {};

// Utility functions for user serialization
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  User.findById(id, function (err, user) {
    done(err, user);
  });
});

// Setup authentication
passport.use(
  new LocalStrategy(options, (username, password, done) => {
    // Get user from database
    User.findOne({ username: username }, (err, user) => {
      if (!user) return done(null, false);
      // Compare passwords
      if (comparePass(password, user.password)) {
        return done(null, user);
      } else {
        return done(null, false);
      }
    });
  })
);

// Utility function to compare passwords with hashed versions
function comparePass(userPassword, databasePassword) {
  return bcrypt.compareSync(userPassword, databasePassword);
}

This sets up passport to work with KoaJS and MongoDB. We’ve defined functions for serialising and deserialising users and setup our authentication strategy. Notice to compare passwords here we’re using bcrypt to compare with the hashed versions.

Next we need to setup authentication within our main application.

Firstly, import it into app.js

/* app.js */
...
const passport = require("koa-passport");
...

Next, setup auth in app.js before the database connection

/* app.js */
...
// Setup authentication
require("./auth"); // Fetches auth file functinos
app.use(passport.initialize()); // Intialises passport authentication
app.use(passport.session()); // Initialises passport sessions
...

Now authentication is setup, we need to create routes to access it.

Create the file routes/auth.js

/* routes/auth.js */

// Setup router
const Router = require("koa-router");
const router = new Router();
const Ctrl = require("../controllers/auth");

// Define routes
router.get("/", (ctx, next) => {
  ctx.body = "Hello Auth!";
});
router.post("/login", Ctrl.login);
router.post("/signup", Ctrl.signup);
router.get("/status", Ctrl.status);
router.get("/logout", Ctrl.logout);

// Export router
module.exports = router.routes();

Notice the routes are all linked to controllers. We haven’t created these yet, so they won’t work for now.

Add these new routes to the router in api.js before exporting the router.

/* routes/api.js */
...
// Add subroutes to main router
router.use("/auth", require("./auth"));
...

Now we will create authentication controllers so that our routes work as intended. This is quite a long file, but will perform the basic actions of signup, login and logout

Create the file controllers/auth.js

/* controllers/auth.js */

// Imports
const User = require("../models/user");
const passport = require("koa-passport");
const bcrypt = require("bcryptjs");

// Login function
async function login(ctx) {
  return passport.authenticate("local", (err, user, info, status) => {
    if (user) {
      ctx.login(user);
      ctx.redirect("/api/auth/status");
    } else {
      ctx.status = 400;
      ctx.body = { status: "error" };
    }
  })(ctx);
}

// Signup function
async function signup(ctx) {
  console.log(ctx.request.body.username);
  // Generate salt
  const salt = bcrypt.genSaltSync();
  // Generate hash using password and salt
  const hash = bcrypt.hashSync(ctx.request.body.password, salt);

  // Create new user document using username and hash
  const newUser = new User({
    username: ctx.request.body.username,
    password: hash,
  });

  // Save user to database
  const savedUser = await newUser.save().catch((err) => console.log(err));
  console.log(savedUser);

  // If user saved correctly, login user
  if (savedUser) {
    // Authenticate user
    return passport.authenticate("local", (err, user, info, status) => {
      // If user is valid
      if (user) {
        // Login user
        ctx.login(user);
        ctx.redirect("/api/auth/status");
      } else {
        // Return error
        ctx.status = 400;
        ctx.body = { status: "error" };
      }
    })(ctx);
  } else {
    // If no user returned, return a bad request
    ctx.status = 400;
  }
}

// Status function
async function status(ctx) {
  // Check whether user is authenticated
  if (ctx.isAuthenticated()) {
    ctx.body = true;
  } else {
    ctx.body = false;
  }
}

// Logout function
async function logout(ctx) {
  if (ctx.isAuthenticated()) {
    // Logout user
    ctx.logout();
    ctx.status = 200;
    ctx.body = "logged out";
  } else {
    // Throw error
    ctx.body = { success: false };
    ctx.throw(401);
  }
}

// Export functions
module.exports = {
  login,
  signup,
  status,
  logout,
};

With all of these controllers working, our application is complete! Check out the key URLs below and the “Going forward” section to see how this can be used.

Key URLs

/api/auth/login - Login a user. Send username and password using JSON in the request body.

/api/auth/signup - Signup a user. Send username and password using JSON in the request body.

/api/auth/status - Get information on whether you are currently logged in.

/api/auth/logout - Logout current user.

/api/ - Base route for API

Request format

Most requests to the endpoints above should follow the below format:

{
	"username": "example_username",
	"password": "example_password"
}

Going forward

To test it out, head over to your command line and run docker-compose up. With any luck, your application should be running. To test this out, we can use a tool such as postman. Simply attach login information in the body of the request as a JSON object, and everything should be working!

From here, you can create your own routes, models and controllers. Using this modularised design will help with any future development, and each section can be tweaked individually. This can be used as a base for any future applications that require an authenticated API. For the complete boilerplate, check out this GitHub repo: https://github.com/oflint-1/koa-auth-boilerplate