Let’s build a FullStack App with JavaScript! ~ Episode 5: Part 2

Matt Basile
Matt’s Lambda Minutes
30 min readMay 21, 2019

This is a multipart series about building a Puppy Adoption Site with JavaScript. If you haven’t read all the articles please find them here: Episode 1| Episode 2 | Episode 3 | Episode 4 | Episode 5-Part 1 | Episode 5-Part 2 | Episode 6

Let’s Build our API — Part 2: Let’s build it up!

Apologies for how long this took to turn around. A combination of family, work, and school have delayed me here, but fortunately, we are back on track. I hope you enjoy!

Introduction

At the completion of Part 1, we established what routes we needed to construct for our API. In Part 2, we’ll go into detail about how we’re going to build these routes!

First, we’ll start by reviewing some setup basics: this includes initializing our Express Router, creating our route and module files and outlining the structure that our route and module functions will follow. Once the basics are set I’ll provide the code for each route, including a request and module function.

To break it down, the article will go like this:

  • Set up our Router
  • Explain and build route and module files
  • Explain the general structure and syntax of the functions we’ll build in the route and module files
  • Build out the routes and modules for the Visitors’ API
  • Build out the routes and modules for the Admin’s API
  • Set up an authorization middleware for Admin API.

As you can see we’ve got a lot to cover here, so let’s dive right in!

Setting up our Router

We currently have our server up and running in our index.js file. We can confirm this by seeing the Woof, Woof! message we programmed to render at the base route.

While this is great for one route, including the logic for each of the 19 unique routes we plan to build in one file is probably not the best idea. As we’ll see when we try writing multiple HTTP requests clarity and simplicity will be key. So choosing to separate our API into specific files will make everything more organized.

To allow this division, we’ll need to create a router connection. This connection forms an extension of our server that we can import to specific files. And as long as we import those routes back to our server, the connection will be created.

Let’s start connecting our router by creating some route files. Go ahead and create a new folder named routes and add two files named visitors-routes.js and admin-routes.js. To start, hop into each file and add these two lines of code.

const router = require("express").Router();module.exports = router;

The remainder of our code will fall between these two lines, but with this code, we are using Express to establish our Router and then export it and all the associated routes we’ll create in these files.

Now that our route files are ready to be used, let’s tell our server where to find them. Hop back over to our index.js file and add these lines of code:

//Below the line const server = express(); addconst visitorRoutes = require('./routes/visitors-routes.js')
const adminRoutes = require('./routes/admin-routes.js')
//Below the line server.use(express.json()); addserver.use("/api/visitors", visitorRoutes)
server.use("/api/admin", adminRoutes)

What we’ve done here is import our routers from the new routes files we’ve created and stored them in the variables visitorRoutes and adminRoutes. Then we’ve instructed our server to use whatever routes are imported from those files at the specified URLs. For instance, any route we create in our visitors-routes.js file must include /api/visitors in front of it when we try to request it from the frontend. The same holds true for the admin routes except we must lead with /api/admin. As will build out specific HTTP requests this relationship between URLs and the frontend will become clearer.

Now that we have our router ready to roll let’s talk about those routes files we built and how they are going to interact with our modules files.

Routes and Modules Files

Earlier we created our routes files and added our server connection to them, but what exactly are we using them for? Our route files will play home to functions that carry out the HTTP requests of our API.

These functions define the URLs our frontend must target to call specific HTTP requests, which when properly called invoke corresponding module functions that update our database. We’ll use a combination of Express commands to define our HTTP requests and the ensuing chain reaction if successfully hit from the frontend.

In our case, a successful hit will trigger one of our tailored module functions. Each module function will interact with our database using SQL commands via Knex methods. Once our module is successful or unsuccessful in accessing our database it will return some form of data back. If the module runs successfully then our route will return the database information that came from the module and a successful HTTP message. If it’s unsuccessful it will return an error and an unsuccessful HTTP message.

This relationship summarized:

  • route functions define our the URLs that must be targeted to initiate specified HTTP requests.
  • If properly targeted it will execute a designated module function, passing any additional information from the frontend with it.
  • The module function interacts with our database, using SQL and Knex, and returns data or an error.
  • The route then processes the values returned from our module and sends the frontend the corresponding information and an HTTP status message.

Now that we have this relationship defined, let’s add some module files, so we’re prepared to start coding.

Adding Module Files

As we learned above each module function will be interacting with our database, so the way I like to structure my module files is to create a file for each table we’re going to access.

In doing so, we reserve all the logic for a single table in one file, helping to create a compartmentalize codebase. Later, we’ll be importing modules into other modules so knowing where each tables’ logic resides is important.

Let’s get started by adding a new folder named modules and proceed by adding the following files:

  • admin-module.js
  • auth-module.js
  • breeds-module.js
  • dogs-module.js
  • kennels-module.js
  • notification-module.js

You might be wondering why there isn’t a visitors module and why there are admin and auth modules. The simple answer is, our breeds, dogs, kennels and notifications modules will include all the logic we need for our Visitors’ API, whereas our Admin’s API requires more tailored modules. The auth module is used to define secure registration and login functions that we’ll explain later in the post.

In each of these files, we’ll end up adding a lot of unique code but to get started, please add these two lines in each:

const db = require("../data/dbConfig.js");module.exports={}

The db variable we’re creating in the first line stores our connection to our database. This connection is stored in our dbConfig file, however, we have not created this file yet. So go ahead and hop in your data folder and add a file name dbConfig.js. Then add these four lines of code in there:

const knex = require('knex');
const config = require('../knexfile.js');
const dbEnv = process.env.DB_ENV || 'development';module.exports = knex(config[dbEnv]);

This code directs our project to the type of database environment we want to use based on our knexfile.js. Currently, we should only have our development environment wired, so everything should rely on our SQLiteDB.

Back to the module function, our module.exports object is where we’ll define the specific functions we want to export from this file. Anything defined here will be accessible by any file that imports the module.

For the moment, that’s all the commonalities we can ensure each module file will share. If we hope to begin customizing and developing these files we first need to understand the basic structure and syntax to our route functions and module functions. Knowing these details will allow us to scale and understand different types of HTTP requests, Express methods, and Knex methods quickly. Let’s dive into those structures so we can begin building our own API!

Route Functions

The best way to learn route functions is to see how we can construct one line by line. Here, I’ll review a GET route that returns all of the dogs in our database.

Step 1

router.get('/dogs', (req, res) => {});
  • router.get() - To begin, we’re using Express’s built-in .get method to define the HTTP request we want our router to use for this function.
  • ('/dogs',- Then in quotations, we are defining the URL that we want our frontend to hit when they need this route. In this example, because we have our router implement the full URL would be api/visitors/dogs.
  • (req,res)=>{}); - Our (req,res) are the shorthand objects for the HTTP request and HTTP response we pass to our function. Our request object can be used to receive information from our frontend. This will be clearer when we begin making POST, PUT and DELETE requests but for more clarity check out these docs. Our response object is what we send back to our frontend whenever we use an Express method. It’s where we can customize the response from our database. For more clarity on response check out these docs.

Step 2

const Dogs = require("../modules/dogs-module.js");router.get('/dogs', (req, res) => {
Dogs.find()
.then(data => {
res.status(200).json(data);
})
.catch(err => {
res.status(500).json({ message: ` Failed to get Dogs `, error: err });
});

});
  • const Dogs = require("../modules/dogs-module.js") - Like we’ve seen before, this line is importing all the modules we’re exporting from the dogs-module.
  • Dogs.find() - Dogs, is our imported dogs-module variable and .find() is the module function we’ve created to return all the dogs in our database. We’ll go over that more in a bit.
  • .then(data=>{res.status(200).json(data);}) - After we call our module, we are then implementing a then/catch block. The then portion will handle a successful response while a catch will respond to errors. Each is designed to received whatever data is passed to it and return it with our response object we passed to the function initially. In this instance, we are using Express’s built-in .status and .json methods to return a successful 200 HTTP status and the remaining data in a digestible JSON format.
  • .catch(err=>{res.status(500).json({message: 'Failed to get Dogs', error: err});}) - Similar to our then block we are parsing the response and returning a message as well. This will provide our frontend some clarification about what is going wrong when there’s an error. In this case, our message is rather generic, but if we wanted to we could unpack the error a bit more and pass a user more detail.

That’s it folks, the basics of creating a route function. We’ll need to parse our request object a bit differently for POST, PUT and DELETE requests, but that’ll be explained in detail when we review each.

Module Functions

For our module functions were going to need to program one for each of the different types of requests we make. As we know, the purpose of each type will be to either retrieve data or make changes to our database.

Eventually, we’ll need to return this information to our frontend and that’s where modules can get a little complicated but very fun. When we make any HTTP request we almost never receive a complete package of the data we desire. For example, when we make a GET request for our dogs' table, the dogs won’t have any breeds attached to them. Another example is when we POST or PUT we only receive an ID back from the database. Unfortunately, this information isn’t always valuable to a frontend developer so it becomes the duty of the API developer to massage the data to provide valid information. Additionally, by providing more information upfront we can prevent unnecessary calls to our server, creating a more pleasant experience for our developers and users.

To ensure we’re getting the most out of the data we’re parsing we’ll rely on some popular solutions to help create more detailed responses:

  • GET Requests — We’ll need to access multiple modules from other tables to return fuller data objects to our frontend. IE Dogs & Breed or Kennels & Dogs.
  • POST Requests — Requires a getById method to find the newly added item in our database.
  • PUT Requests — Requires a getById method to find the newly edited item in our database.
  • DELETE Request — Returns a 1 if successful. We won’t be editing the response for the frontend, but in the future, we could implement a tailored message depending on the value.

To get a better idea of how these tips permeate a module function let’s break down the find() function from our route example:

Step 1

async function find(){}
  • async- We use async to make our function asynchronous. Asynchronicity is important because it allows us to process multiple actions within a single function. This is necessary here because we’ll be making calls to other functions and we’ll want to return all the data before proceeding forward. This will also rely on the await keyword to ensure all our data is being returned properly. A better explanation of this relationship can be found here.
  • function find()- Here we are simply defining our function and not passing anything into it.

Step 2

async function find() {
const dogs = await db("dogs")
}
  • Here we’re obtaining the values from our database(db) dogs’ table and storing it in our dogs variable. As mentioned above, the included await allows us to wait for all the values to be return from our dogs table before storing it in our dogs variable.
  • While this is totally functional, as we are successfully returning all of the dogs in our database. However, they would not have their breeds attached to them!

Step 3

async function find() {
const dogs = await db("dogs")
var fullDogs = []
for(i=0; i<=dogs.length; i++){
const dog = await findById(dogs[i].id)
fullDogs.push(dog)
}
return fullDogs

}
  • var fullDogs =[]- first we initially an empty array so that we can store our complete dog objects in there.
  • for(i=0; i<=dogs.length; i++){const dog = await findByID(dogs[i].id) fullDogs.push(dog)} - We create a for loop to cycle over each dog data entry and pass it to our findById function. Our findById function will search for that individual dog and provide a robust dog object complete with breeds. Also, we implement await again to ensure we’re receiving all the information back. Once completed we are pushing that freshly return dog object to our fullDogs array.
  • return fullDogs - Finally, we return our entire fullDogs array to be processed by our route function.

We’ll be mimicking this structure as we create module functions that look to return the most information possible. As we listed above, depending on the specified HTTP request it will dictate how we invoke other functions to refine our data, but the key concept is we’re trying to optimize our modules by avoiding unnecessary calls to our server.

With all this outlined, let’s dive into our Visitors’ and Admin’s APIs and get things rolling!

Visitor’s API

For the API portion of this article, I’m going to select a specific route we wanted to create from Part 1. Then I’ll share the code for the specific route function, analyze any new syntax and then do the same for any module functions used. I believe this structure will allow us to move through the routes and modules quickly while still absorbing as much knowledge as possible.

GET — api/visitors/kennels

Route Code: In visitors-routes.jsconst Kennels = require("../modules/kennels-module.js")router.get('/kennels', (req, res) => {
Kennels.find()
.then(data => {
res.status(200).json(data);
})
.catch(err => {
res.status(500).json({ message: "Failed to get Kennels", error: err });
});
});
  • Our classic GET route function, almost identical to what we programmed in our demo above. Be sure to import the Kennels module at the top of your file before starting.
Module Function: In kennels-module.jsconst Dogs = require("./dogs-module.js")
module.exports ={
find,
findById
}
async function find() {
const kennels = await db("kennels")
const fullKennel = await Promise.all(kennels.map(kennel => findById(kennel.id)))
return fullKennel;
}
async function findById(id) {
const kennel = await db("kennels").where({id}).first();
const doggos = await db("dogs").where({"kennel_id": id})
const fullDogs = await Promise.all(doggos.map(dog => Dogs.findById(dog.id)))
return {...kennel, dogs:fullDogs}
}
  • Our find() function is similar to our demo above, except instead of creating a for loop to store our data, we are using the .map() method.
  • In our .map() method we are passing each kennel’s id to our findById function.
  • In our findById function we find that kennel again, using the .where() and first() methods to ensure we’re finding the first instance of a data entry where our id is the same. Similarly, we’re using that same .where() to locate all the dogs that are linked to this kennel.
  • Once we have our dogs, we are mapping over them and passing them to our Dogs.findById() function. This module is imported at the top of file with the syntax const Dogs = require(“./dogs-module.js”).
  • Once that is completed, we are then returning it by spreading the kennel information throughout the object and return our fullDogs array with a dogs keys. Every kennel in our database will go through this looping process.

GET — api/visitors/kennels/:id

Route Code: In visitors-routes.jsrouter.get('/kennels/:id', (req, res) => { 
Kennels.findById(req.params.id)
.then(data => {
res.status(200).json(data);
})
.catch(err => {
res.status(500).json({ message: "Failed to get Kennel", error: err });
});
});
  • Almost identical to above, except this time we are passing an id in our URL. We access this id using req.params.id and passing it to our findById function. For more on req.params checkout the Express docs.
Module Function: In kennels-module.jsFortunately we programmed this above for our find() function.

GET — api/visitors/dogs

Route Code: In visitors-routes.jsconst Dogs = require("../modules/dogs-module.js");router.get('/dogs', (req, res) => {
Dogs.find()
.then(data => {
res.status(200).json(data);
})
.catch(err => {
res.status(500).json({ message: "Failed to get Dogs", error: err });
});
});
  • We’ve seen this before right? This is the exact route we used for our demo! Nothing new to add except, remember to import the Dogs module!
Module Function: In dogs-module.jsmodule.exports ={
find,
findById
}
async function find() {
const dogs = await db("dogs")
var fullDogs =[]
for(i=0; i<dogs.length; i++){
const dog = await findById(dogs[i].id)
fullDogs.push(dog)
}
return fullDogs;
}
async function findById(id) {
const dog = await db("dogs").where({id}).first();
const breedID = await db("dog_breeds").innerJoin('dogs', 'dogs.id', 'dog_breeds.dog_id').where({"dog_id": dog.id})
const breeds = await Promise.all( breedID.map(async(breed)=>{
try {
const name = await findBreed(breed.breed_id)
return name[0].name
}
catch (error) {
console.log(error)
}
})
)
return {...dog, breeds}
}
function findBreed(id){
return db("breeds").where({id})
}
  • Ok so our find() function we’ve seen before but our findByID has got a lot going on… let’s break down the new stuff.
  • const breedID = await db(“dog_breeds”.innerJoin(‘dogs’, ‘dogs.id’, ‘dog_breeds.dog_id’).where({“dog_id”: dog.id}) this line of code isn’t dissimilar from what we’ve been doing so far. Except, we are using a Knex method,.innerJoin, to join the data we’re receiving from our dogs, and dogs_breeds tables. Ultimately, we’re selecting all the instances where a dog’s id is present in the dog_breeds table and joining that to the existing dogs object. Creating a new key in our object that holds a dog’s breed_id.
  • We then use this new breed_id to our findBreed function, which locates that breed and returns it. We then parse the breed we’re receiving and just select the name value from it. We include this logic in a try/catch block. The try/catch provides security that in case our function breaks inside of our map() method we won’t get caught in an infinite loop.
  • Then we return all that information back by spreading our dog information and creating a breeds key that stores an array of that dogs breed names.
  • Every dog in our database will go through this process when we call our find() function.

GET — api/visitors/dogs/:id

Route Code: In visitors-routes.jsrouter.get('/dogs/:id', (req, res) => {
Dogs.findById(req.params.id)
.then(data => {
res.status(200).json(data);
})
.catch(err => {
res.status(500).json({ message: "Failed to get Dog", error: err });
});
});
  • Nothing new here, it follows the same pattern from our api/visitors/kennels/:id route.
Module Function: In dogs-module.jsFortunately we programmed this above for our find() function.

GET — api/visitors/breeds/:id

Route Code: In visitors-routes.js
const Breeds = require("../modules/breeds-module.js");
router.get('/breeds/:id', (req, res) => {
Breeds.findBreeds(req.params.id)
.then(data=>{
res.status(200).json(data)
})
.catch( err=> {
res.status(500).json({ message: "Failed to get Breed", error: err});
})
});
  • Again, same syntax as the id route functions we’ve created above. Be sure to import our Breeds module!
Module Function: In breeds-module.jsconst Dogs = require("./dogs-module.js");module.exports={
findBreeds,
}
async function findBreeds(id){
const middle = await db("dog_breeds").where({"breed_id":id})
const dogs = await Promise.all(
middle.map(async(dog)=>{
const pup = await Dogs.findById(dog.dog_id)
return pup
})
)
const { name } = await db("breeds").where({id}).first()
return {breed: name, dogs}
}
  • In our dogs’ module, we need to retrieve all the associated breeds for a dog so for our breeds module, it’d be nice if we did the same.
  • First, we’re locating all the connections between the breed we’ve selected and the dogs in our dog_breeds table. We then store it in the middle variable.
  • Then we are mapping over that value and searching for any dog that corresponds to that relationship.
  • We then are retrieving the name of the breed we are searching for, and passing that back with an array of dogs who all have the same breed as the one we are searching for.

POST — api/visitors/notifications

Route Code: In visitors-routes.js
const Notifications = require("../modules/notifications-module.js");
router.post('/notifications', (req, res) => {
const date_sent = Date.now()
const notification = {...req.body, date_sent}
Notifications.add(notification)
.then(data=>
res.status(201).json(...data)
)
.catch(err =>
res.status(500).json(error: err, message: "Unable to post notification. Please make sure to include your admin_id and dog_id"))
});
  • Our first POST route! When we POST anything, that means we are retrieving data from our frontend and storing it in our database. We do this by passing the information in our req.body. I’ll explain a bit more about this when we construct our frontend, but when we pass information in our req.body we need it to match the data structure of the table we’re trying to POST to. So in this instance, our req.body needs to include a admin_id and a dog_id. Those are required or else it will not be added to our database.
  • However, that is just the minimum values we need! We can accept an email, name, message and date_sent to store as well. To normalize the date_sent format, I’ve decided to handle that on the backend by including the line const date_sent = Date.now(). We then combine it with whatever other data we receive in our req.body and pass that off to our add function.
  • The only major differences between this and any of our find functions are the 201 status message and the tailored error message. 201, is the proper response message when something is successfully created in a database. If you want to learn more about HTTP messages I suggest looking at these doggos for some help.
  • The custom error message is to remind a user of what they definitely need to create a data entry. While this may not be the only reason we throw an error during this process it might result in a quick fix for those who are forgetting those values.
Module Function: In notifications-module.jsmodule.exports = {
add,
};
async function add(notification) {
const [id] = await db('notifications').insert(notification);
return findById(id);
}
function findById(id){
return db('notifications').where({id})
}
  • Nothing too foreign here. The two big points to highlight are the .insert() method and the [id] syntax.
  • .insert() method is a Knex method that creates an insert query for us to seamless add data to the table we define.
  • We then are given the Id of the successfully posted notification. Then using the bracket notations we extract the Id value from the returned array, store it in the id variable and pass it to our findById function, which we’ve all seen before!

There you have it, all the routes for our Visitor’s API. A complete Gist of these will be included once we finish our Admin API… which leads me to the next section!

Admin’s API

Our Admin API is very similar to our Visitors API, except we deal with a few more HTTP requests and some new login and registration logic. We’ll start with the familiar and then I can dive into what we’ll need to do to create a secure login and registration system.

GET — api/admin/:id

This is our api/kennels/:admin-id route. Instead of a kennel returning all the admin information, it’s an admin returning all their kennel information. A semantical decision but it makes more sense with our Admin structure.

Route Code: In admin-routes.jsconst Admins = require('../modules/admin-module.js');router.get('/:id', (req, res) => {
Admins.findById(req.params.id)
.then(data =>{
res.status(200).json(data)
})
.catch(err =>{
res.status(500).json(error:err, message:"Unable to locate Admin")
})
});
  • Again nothing unusual going on here, more of the same syntax we’ve applied earlier.
Module Function: In admin-module.js
const Kennels = require('./kennels-module');
const Dogs = require('./dogs-module');
module.exports = {
findById,
}
async function findById(filter) {
const id = Number(filter)
const [admin] = await db("admins").where({id})
const [kennel] = await db("kennels").where({"id": admin.kennel_id})
const dogs = await Dogs.findByKennel(kennel.id)
return {...admin,...kennel, dogs}
}
FindByKennel Function: In dogs-module.js

async function findByKennel(kennel_id){
const dogs = await db("dogs").where({kennel_id});;
var full =[]
for(i=0; i<dogs.length; i++){
const dog = await findById(dogs[i].id)
full.push(dog)
}
return full
}
  • Our findById function is nothing we haven’t seen before, but let’s break it down just so we’re clear what’s going on.
  • If you notice we initially pass in our id as a filter variable and then convert it to a number. When testing this route the value being passed was not being read as a number, so in the first line, we convert it to one.
  • We then use our id, to search for that admin, which we then store in the admin variable. Using this data, we find our corresponding kennel, which we store so we can find the corresponding dogs. Then we return our admin, kennel and dog information back to our front end.
  • We needed to add a findByKennel function to ourdogs-module so that we could find all dogs that correspond to a specified kennel. The function we created is very similar to our find() function in the dog-module, except we are targeting all the dogs where their kennel_id matches.

DELETE — api/admin/:id

This is a route that was not included in the previous article but is probably something we want to offer our users. If someone wants to delete their account they should be able to and this route is how we’ll reflect those desires in our database.

Route Code: In admin-routes.jsrouter.delete('/:id', (req, res) => {
Admins.remove(req.params.id)
.then(data => {
res.status(200).json(data)
})
.catch(error => {
res.status(500).json(error)
})
});
  • Very similar to a lot of the routes we’ve programmed so far, we just need to make sure we are passing the req.params.id to our function.
Module Functions: In admin-module.jsmodule.exports = {
remove,
};
async function remove(id){
const admin = await findBy({id})
await Kennels.remove(admin.kennel_id)
const dogs = await Dogs.findByKennel(admin.kennel_id)

dogs.forEach(dog =>{
removeDog(dog.id)
})
return await db("admins").where({id}).del()
}
async function removeDog(id){
return db("dogs").where({id}).del();
}
Remove Kennel Function: In kennels-module.jsfunction remove(id){
return db("kennels").where({id}).del();
}
  • Since all of our Admins will be associated with a kennel we need to make sure we’re removing both when we delete one. That way there’s no empty responses when dogs are requested for adoption.
  • To ensure this, we first use the id we pass on the frontend so we can find our specified admin’s kennel_id. Then using that we remove our kennel, using the remove function from our kennel’s module and then we finally remove our admin.
  • Next, we take that same kennel_id and use it to find all the dogs that belong to that kennel. Since we don’t want folks adopting dogs from kennels that don’t exist we loop over each dog and remove them from the dogs' table.

POST — api/admin/register

Our register function is going to require two dependencies that we haven’t installed yet. Depending on your build, hop in your terminal and add bcryptjs and jsonwebtoken.

YARN: yarn add bcrycptjs jsonwebtoken

NPM: npm install bcrycptjs jsonwebtoken

For more information on the dependency themselves check out the docs here: bcrypt, jsonwbetoken.

These two dependencies will allow us to hash our passwords and create tokens so that we can securely transfer password information across our server and to ensure the designate user is logged in by providing the proper token.

Route Code: In admin-routes.jsconst Auth = require('../modueles/auth-module.js')router.post('/register', (req, res) => {
const user = req.body
if(!user.username && !user.password ) {
res.status(401).json({ message: "Please provide a username and password for this user."})
}
else{
Auth.register(user)
.then(data => {
res.status(200).json(data)
})
.catch(err => {
res.status(500).json({ message: ` Failed to register `, error: err });
});
}
});
  • As you can see this looks a lot like our traditional POST request, except we’ve added an initial if/else statement to ensure that we’re receiving both a username and password from our users.
Module Functions: In auth-module.js
const Admins = require('./admin-module.js');
const Kennels = require('./kennels-module.js');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const jwtKey = process.env.JWT_SECRET || 'this is just a test';
const secret = process.env.JWT_SECRET || 'this is just a test';
module.exports = {
register,
};
async function register(user) {
let { username, password } = user
const name = user.kennel_name
//implement user registration
const hash = bcrypt.hashSync(password, 12); // 2 ^ n
password = hash;
const addKennel = await Kennels.add({name})
const saved = await Admins.add({username, password, 'kennel_id': addKennel.id})
try {
return ({token: generateToken(saved), id: saved.id })
}
catch (error) {
return error
}
}
//Generate Token
function generateToken(user){
const payload = {
id: user.id,
username: user.username,
};
const options ={
expiresIn: '24h',
};
return jwt.sign(payload, secret, options)
}
  • A lot to digest here, but let’s start from the top. The first four lines we’ve seen before, just importing our other modules and our two dependencies. The exporting our register function in our modules.export.
  • We then store a json web token key and a secret using the JWT_SECRET from our .env file. If you look through our code, our secret isn’t called to the very last line in the jwt.sign() method. This secret will come into play when we create an authenticate middleware and we need to check that tokens being passed to us were created by us. To this point, we’ve created a lot of routes in our API but anyone who comes to our site can make a call to them.
  • Well, we don’t want that! Instead, we want admins to have very specific routes they can target. To ensure this we can create an authenticate middleware that will require a user’s token. This token will have our secret embedded in it and without going through the proper registration or login token generation they’ll never have access to it.
  • Our user object we pass to our function requires three parameters: username, password and kennel_name. We include a kennel_name because we want every admin to immediately be associated with a kennel. To ensure this, once we receive the kennel_name we are creating a kennel and storing the returned kennel_id with our username and password to add to our admin’s table.
  • Prior to adding our admin, did you notice he line const hash = bcrypt.hashSync(password, 12)? It looks tricky but we’re using bcrypt’s hashSync method, to hash our password. The 12 we pass along with it indicates how many times we want our password to be hashed. This iterative approach is what makes bcrypt so secure. That’s because we’re not just looping over it 12 times but hashing and rehashing 2¹² times. With each loop, our password grows increasingly difficult for hackers to guess.
  • Once we’ve successfully generated our admin, we return that to our frontend with a token. However, we need to generate a token, to do so we create the generateToken() function that takes a few pieces of our user’s data and an expiration time for our token and pass those to be signed with our secret. By combining these three we are creating a unique verified credential for this user and as long as this token is active the user will have access to their information and site.

POST — api/admin/login

For every good registration route, there’s an equally great login route behind it! Let’s break it down and see what’s going on.

Route Code: In admin-routes.jsrouter.post('/login', (req, res) => {
const {username, password} = req.body
Auth.login(username)
.then(user => {
if (user && bcrypt.compareSync(password, user.password)) {
const token = Auth.generateToken(user);
res.status(200).json({
message: `Welcome ${user.username}!, have a token...`,
token,
id: user.id,
});
}
else {
res.status(401).json({ message: 'Invalid Credentials' });
}
})
.catch(error => {
res.status(500).json(error);
});
});
  • This route function looks interesting because we do a lot of password confirmation within it. Initially, you’ll see we parse our username and password from our req.body. Then we only pass our username to the login method. As well see shortly, our login method checks to see if that user exists.
  • We then enter our if statement, which confirms that the user exists, and the password submitted is equal to the stored password. We compare the password in bcrypt.compareSync() method relying on bcrypt to unhash our stored password to compare to our initial input.
  • Once this is confirmed we generate a token and hand that back to our frontend with a tailored message. However, if our initial password or user check fails, we’ll return an Invalid Credentials message and status response.
  • Lastly, our 500 response is there in case something goes wrong when making our initial search to the back end.
Module Functions: In auth-module.jsmodule.exports = {
login,
};
async function login(username) {
// implement user login
return await Admins.findBy({username})
}
FindBy Function: In admin-module-jsmodule.exports = {
findBy,
};
async function findBy(filter){
return await db("admins").where(filter).first()
}
  • Since most of our logic is handled in our route model our login function is fairly simple. However, we do use a new admins module, findBy, in order to track down our user by their username.
  • Our findBy, function is pretty straight forward as we search our admins table for the first instance that matches our filter.

PUT — api/admin/:id

This is another route that was not included in the previous article, but something we should have. At some point, a user may want to update their username or password and this route will take care of that.

Route Code: In admin-routes.jsrouter.put('/:id', (req, res) => {
const changes = req.body
Admins.updateAdmin(req.params.id, changes)
.then(data=>
res.status(200).json(data)
)
.catch(err=>{
res.status(500).json(err)
})
});
  • PUT calls are very similar to our POST calls in that we receive a chunk of data from our frontend and pass it to our database.
  • In this instance not only do we need the changes but we also need to use req.params.id to target the user we need to change.
Module Functions: In admin-module.jsmodule.exports = {
updateAdmin,
};
async function updateAdmin(id, changes){
if(changes.password){
const hash = bcrypt.hashSync(changes.password, 12); // 2 ^ n
changes.password = hash;
}
return await db('admins').where({id}).update(changes)
.then(count => (count > 0 ? findById(id): null))
}
  • In this function, we use a new Knex command called .update to complete the updates within our database.
  • First, we need to check that if a user is updating their password we need to hash it before we store it. That way when we generate tokens and ensure secure logins every password in our database will be hashed.
  • At the end of this call, we are tagging on a .then operation because when we complete a PUT request we’ll receive either a 1 or 0. A 1 indicates a successful update so when that occurs we’ll use our findById method to return all the update information from our database.

PUT — api/admin/kennels/:id

Above, when we update our admin, that strictly focuses on their login requirements. However, we’re going to need a route so they can update their kennels as well. Here’s how we can implement that:

Route Code: In admin-routes.jsrouter.put('/kennels/:id', (req, res) => {
const changes = req.body
Admins.updateKennel(req.params.id, changes)
.then(data=>
res.status(200).json(data))
.catch(err=>{
res.status(500).json(err)
})
});
  • Nothing new here, very similar to our admin PUT call. We need to pass in the id and changes from the frontend.
Module Functions: In admin-module.jsmodule.exports = {
updateKennel,
};
async function updateKennel(id, changes){
return await db('kennels').where({id}).update(changes)
.then(count => (count > 0 ? Kennels.findById(id): null))
}
  • As you can see this is almost identical to updating our Admin. We’re locating that specific kennel, updating the changes, and then return the update kennel if everything goes successfully.

As we proceed we won’t be programming a GET, POST or DELETE route for kennels. And the reason we don’t have them deserves an explanation:

  1. GET — Our GET calls are programmed in our Visitors’ API and fortunately our admins will also have access to that.
  2. POST — For the time being, we only want a single admin to be associated with a single kennel. However, the way in which we’ve structured our database will allow us to iterate and change that functionality if admins need to manage multiple kennels in the future.
  3. DELETE — Similarly, a Kennel should only be deleted when an admin is, so for that reason, there is no kennel specific delete function.

POST — api/admin/dogs

Our kennels need dogs right? Our admins are going to be the ones to add them using this logic here:

Route Code: In admin-routes.js
const Dogs = require('./modules/dogs-module.js');
router.post('/dogs', (req, res) => {
const dog = req.body
Dogs.add(dog)
.then(data=>
res.status(201).json(data))
.catch(err=>{
res.status(500).json(err)
})
});
  • Again, more of the same, just remember to import our dog’s module to access our Dogs.add() function!
Module Functions: In dogs-module.jsmodule.exports = {
add,
};
async function add(dog){
const [id] = await db('dogs').insert(dog);
return findById(id);
}
  • Adding a dog has never been easier! Exactly, what we’d expect to see relative to our project.

PUT — api/admin/dogs/:id

Route Code: In admin-routes.jsrouter.put('/dogs/:id', (req, res) => {
const changes = req.body
Admins.updateDog(req.params.id, changes)
.then(data=>
res.status(200).json(data)
)
.catch(err=>{
res.status(500).json(err)
})
});
  • Our project is literally becoming this meme:
Module Functions: In admin-module.js
module.exports = {
updateDog,
};
async function updateDog(id, changes){
return await db('dogs').where({id}).update(changes)
.then(count => (count > 0 ? Dogs.findById(id): null))
}
  • Reference photo above.

DELETE — api/admin/dogs/:id

Route Code: In admin-routes.jsrouter.delete('/dogs/:id', (req, res) => {
Admins.removeDog(req.params.id)
.then(data =>{
res.status(200).json(data)
})
.catch(error =>{
res.status(500).json(error)
})
});
  • Reference DELETE Admin or Kennel for explanation
Module Functions: In admin-module.js
module.exports = {
removeDog,
};
async function removeDog(id){
return db("dogs").where({id}).del();
}
  • Reference DELETE Admin or Kennel for explanation

POST — api/admin/breeds/assign

This route is going to be important when we want to add a breed to a dog. Ideally, when an admin adds a dog they’ll be uploading breed information as well. However, if we later discover this dog has a certain breed, this will allow us to update it.

Route Code: In admin-routes.jsrouter.post('/breeds/assign', (req, res)=>{
const {dog_id, breed_id} = req.body
Admins.assignBreed(dog_id, breed_id)
.then(data=>{
res.status(201).json(data)
})
.catch(err=>{
res.status(500).json(err)
})
})
  • The purpose of this POST call is to assign a dog a specific breed. To do that we need to create a relationship on our dog_breeds table. The data entry requires a dog_id and breed_id and since we have that available to us on the frontend, we pass that along to our database.
Module Functions: In admin-module.jsmodule.exports = {
assignBreed,
};
async function assignBreed(dogID, breedID){
const response = await db("dog_breeds").insert({"dog_id": dogID, "breed_id": breedID})
if(response > 0){
return await Dogs.findById(dogID)
}
else{
throw new Error('Breed or Dog does not exist')
}
}
  • Here we take our assigned variables and pass them to our database as an object. We then store that response (1 for success, 0 for fail) in the response variable.
  • We then check that we got a positive response back and proceed to find that dog. The dog will have the updated breeds attached to it at it’s breeds array.

POST — api/admin/breeds/:id

This POST will allow us to add a new dog breed anytime we add a new dog to our database.

Route Code: In admin-routes.jsrouter.post('/breeds/:id', (req, res) => {
const dog_ID = req.params.id
const breed = req.body
Admins.addBreed(breed, dog_ID)
.then(data=>{
res.status(201).json(data)
})
.catch(err=>{
res.status(500).json(err)
})
});
  • A little change up here is that we’re passing our dog’s id in req.params.id not our breeds. This is a fine maneuver as long as we document it in our READMEs
Module Functions: In admin-module.jsmodule.exports = {
addBreed,
};
async function addBreed(breed, dog_ID){
const [id] = await db("breeds").insert(breed)
await db("dog_breeds").insert({"dog_id": dog_ID, "breed_id": id})
return await Dogs.findById(dog_ID)
}
  • First, we take our breed information and insert that into our breeds table. This returns an id which we then use to insert, with our dog_ID, into our dog_breeds table to create a relationship between that dog and breed.
  • Finally, we search for that specific dog, using our findById function, which returns our updated dog with all it’s associated breeds.

POST — api/admin/breeds/remove

Sometimes we’ll misassign a dog’s breed and need to rectify it. This route will allow us to do that.

Route Code: In admin-routes.jsrouter.delete('/breeds/remove', (req, res) => {
const {dog_id, breed_id} = req.body
Admins.removeBreed(dog_id, breed_id)
.then(data =>{
res.status(200).json(data)
})
.catch(error =>{
res.status(500).json(error)
})
});
  • Here we’re doing more of the same, extracting our variables passed in our body and then passing those to our backend to help identify what needs to be deleted.
Module Functions: In admin-module.jsmodule.exports = {
removeBreed,
};
async function removeBreed(dog_id, breed_id){
const response = await db("dog_breeds").where({'dog_id': dog_id, 'breed_id':breed_id}).first().del();
if(response > 0){
return await Dogs.findById(dog_id)
}
else{
throw new Error('Breed or Dog does not exist')
}
}
  • History has a funny way of repeating itself huh? This function is almost identical to our assignBreed function except for two major components.
  • First, we’re using del() instead of insert(). But to identify our dog/breed relationship we are passing both our dog_id and breed_id to our filter and then using our .first() method to find the first instance of it. Luckily, the first instance will be the only on our table.

POST — api/admin/notifications/:id

Lastly, we need a way for our admins to check who’s been trying to get in touch with them. Our notifications route will return all the notifications.

Route Code: In admin-routes.jsrouter.get('/notifications/:id', (req, res) => {Admins.getNotifications(req.params.id)
.then(data=>
res.status(200).json(data))
.catch(err=>{
res.status(500).json(err)
})
});
  • We’ve seen this before! If you’re confused, please reference any of the previous GET requests.
Module Functions: In admin-module.jsmodule.exports = {
getNotifications,
};
function getNotifications(id){
return db('notifications').where({"admin_id":id})
}
  • That’s the easiest route we’ve seen today, so nice we saved it for last!
  • Just note that in our notifications table we store our admin_id using that syntax so converting it into an object was important to target it.

We did it, we finished our API! All those routes and modules are going to make everything run smoothly and safely! However, there is one last function we need to add.

As we’ve mentioned prior our API is split up into a Visitor’s and Admin portion. The main reason for this delineation is the responsibilities that admins have access to are much more sensitive than any plain visitor to our site. With that said the routes our admins are hitting need to be secured further by an authentication middleware.

Our middleware function will live in our auth-module.js file and it’s role will be to interpret tokens. If there’s a valid token passed to it, it will allow that user to access any route they are attempting to hit. If not, it will reject that user and any actions they try to preform. This function looks like this:

Module Functions: In auth-module.jsfunction authenticate(req, res, next) {
const token = req.get('Authorization');
if (token) {
jwt.verify(token, jwtKey, (err, decoded) => {
if (err) {
res.status(401).json(err);
}
else{
req.decoded = decoded;
next();
}
})
}
else {
res.status(401).json({ error: 'No token provided, must be set on the Authorization Header'});
}
}
  • Remember during our login and registration we returned a token? We’ll that’s so we can store it on the frontend and use it to access routes protect by this middleware. The way we do that is by creating an HTTP header field named Authorization and pass it to our backend. That way this middleware uses req.get(‘Authorization’) to find it and store it as token. We then rely on jwt (JSON Web Token) to verify that our token’s information matches our jwtKey. Ultimately, this confirms that this token is one we created and has not been tampered with. If it’s been tampered with or doesn’t match an error will be thrown. Then if everything checks out, our token passes and we use next() to move on to our next function.

For the time being, we don’t want to add our authenticate middleware to our code because when we go through testing and building the frontend it will be a nightmare. However, before we go live I’ll show you exactly where to place it!

Now I swear, there are no more hidden tricks: we are done, we are done!

What a ridiculous GIF of the Colonel?!?

Thank you for following along and remaining patient with me as I finished this post! If you want to see all the code above consolidate in one place feel free to check out this repo or the code snippets below.

Github Repository: Doggo-Land

Snippets:

Routes -

Modules -

Thanks again for following along, and I hope to have the first part of our frontend out soon!

--

--

Matt Basile
Matt’s Lambda Minutes

Full Stack Developer sharing updates of his journey through Lambda School’s course. Check-in for a recap of core concepts and how-to guides.