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:
- Npm and node
- Docker
Technologies used:
- KoaJS (Javascript framework used to create the API)
- Passport (Provides authentication)
- MongoDB + Mongoose (This will be the database for the API)
- Docker + DockerCompose (To connect different parts of the app)
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