Build a Rest API for Node & Mysql 2018 JWT

Brian Schardt
codeburst
Published in
11 min readJan 24, 2018

--

Javascript is a hard language to get right, and I am tired of all the tutorials that build Node APIs in a way that is not maintainable. So I have decided to build my own, based off the design of Php’s golden framework Laravel.

Description of App: This is an Restful API for Node.js and Mysql. I have also written an article for an API in Node and Mongo. Click here for that one.

UPDATE: Graphql api’s(created by facebook) are better for 90% of apps than restful api’s. I highly recommend learning about graphql. Here is a link to a template I have built for them. Click Here!

This is in the MVC format, except since it is an API there are no views, just models and controllers. Routing: Express, ORM/Database : Sequelize, Authentication : Passport, JWT. The purpose of using JWT (Json Web Token) is for the ease at which it integrates with SPAs( like Angular 2+, React, etc), and Mobile applications. Why build a separate API for each, when you can build one for both?

Click Here for a Front End that uses this backend for authentication.

This tutorial assumes you have intermediate knowledge of mysql and node. THIS IS NOT A TUTORIAL FOR A BEGINNER.

If you have any questions or suggestions I will try to respond within the hour! I promise!

Beginning — Download the Code

The code is on Github, and I highly recommend cloning the repo first to follow along. Click here for the repo link. Clone the repo, and then install the node modules.

App Structure

The structure uses the standard express app structure, combined with how sequelize organizes things, along with some Laravel structure.

— bin
— config
- - - config.js
— controllers
- - - company.controller.js
- - - home.controller.js
— — — user.controller.js
— middleware
- - - custom.js
- - - passport.js
— models
— — — index.model.js
— — — company.model.js
— — — user.model.js
— public
— routes
— — — v1.js
- seeders
- services
- - - auth.service.js
- - - util.service.js
.env
app.js

Lets get into the Code

Lets start with .env

Rename example.env to .env and change it to the correct credentials for your environment.

APP=dev
PORT=3000

DB_DIALECT=mysql
DB_HOST=localhost
DB_PORT=3306
DB_NAME=dbNameChange
DB_USER=rootChange
DB_PASSWORD=passwordChange

JWT_ENCRYPTION=PleaseChange
JWT_EXPIRATION=10000

Instantiating Environment Variables

config/config.js

Grabs env from .env file, and makes a standard way of accessing them throughout the app, and gives them default variables if an environment variable is not found.

require('dotenv').config();//instatiate environment variables

let CONFIG = {} //Make this global to use all over the application

CONFIG.app = process.env.APP || 'dev';
CONFIG.port = process.env.PORT || '3000';

CONFIG.db_dialect = process.env.DB_DIALECT || 'mysql';
CONFIG.db_host = process.env.DB_HOST || 'localhost';
CONFIG.db_port = process.env.DB_PORT || '3306';
CONFIG.db_name = process.env.DB_NAME || 'name';
CONFIG.db_user = process.env.DB_USER || 'root';
CONFIG.db_password = process.env.DB_PASSWORD || 'db-password';

CONFIG.jwt_encryption = process.env.JWT_ENCRYPTION || 'jwt_please_change';
CONFIG.jwt_expiration = process.env.JWT_EXPIRATION || '10000';

module.exports = CONFIG;

Main file app.js

Require dependencies, and instantiate server.

const express      = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const passport = require('passport');
const pe = require('parse-error');
const cors = require('cors');

const v1 = require('./routes/v1');
const app = express();

const CONFIG = require('./config/config');

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

//Passport
app.use(passport.initialize());

Connect to Database and Load models

const models = require("./models");
models.sequelize.authenticate().then(() => {
console.log('Connected to SQL database:', CONFIG.db_name);
})
.catch(err => {
console.error('Unable to connect to SQL database:',CONFIG.db_name);
});
if(CONFIG.app==='dev'){
models.sequelize.sync();
// models.sequelize.sync({ force: true });
}

CORS — SO other websites can make requests to this server *Important

app.use(cors());

Setup Routes and handle errors

app.use('/v1', v1);

app.use('/', function(req, res){
res.statusCode = 200;//send the appropriate status code
res.json({status:"success", message:"Parcel Pending API", data:{}})
});

// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

module.exports = app;

Set Up Promise Handler in app.js

process.on('unhandledRejection', error => {
console.error('Uncaught Error', pe(error));
});

Utility Service — services/util.service.js

These are helper functions that we will use through out the app. The “to” function helps with handling promises and errors. It is a super helpful function. To read more about its purpose click here. The ReE and ReS functions help the controllers send responses in a unified way.

const {to} = require('await-to-js');
const pe = require('parse-error');

module.exports.to = async (promise) => {
let err, res;
[err, res] = await to(promise);
if(err) return [pe(err)];

return [null, res];
};

ReE, ReS — Standard way of sending responses

The purpose this is to make sure every successful and error response is sent in the same format.

module.exports.ReE = function(res, err, code){ // Error Web Response
if(typeof err == 'object' && typeof err.message != 'undefined'){
err = err.message;
}

if(typeof code !== 'undefined') res.statusCode = code;

return res.json({success:false, error: err});
};

module.exports.ReS = function(res, data, code){ // Success Web Response
let send_data = {success:true};

if(typeof data == 'object'){
send_data = Object.assign(data, send_data);//merge the objects
}

if(typeof code !== 'undefined') res.statusCode = code;

return res.json(send_data)
};

TE is basically a short cut for being able to quickly throw errors

module.exports.TE = TE = function(err_message, log){ // TE stands for Throw Error
if(log === true){
console.error(err_message);
}

throw new Error(err_message);
};

Setup database and load models.

models/index.js

'use strict';

var fs = require('fs');
var path = require('path');
var Sequelize = require('sequelize');
var basename = path.basename(__filename);
var db = {};

const sequelize = new Sequelize(CONFIG.db_name, CONFIG.db_user, CONFIG.db_password, {
host: CONFIG.db_host,
dialect: CONFIG.db_dialect,
port: CONFIG.db_port,
operatorsAliases: false
});

connect to sequelize using env variables

Load all the models in the model directory

fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
var model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});

Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});

Export Sequelize

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

User Model

Import User Model — models/user.model.js

'use strict';
const bcrypt = require('bcrypt');
const bcrypt_p = require('bcrypt-promise');
const jwt = require('jsonwebtoken');
const {TE, to} = require('../services/util.service');
const CONFIG = require('../config/config');

Build Schema with hooks and custom methods. A hook is function you can add when something happens in sequelize. Here we use it to hash the password every time it is change using the beforeSave hook.

We also have a custom method on the model to generate a JWT token for this user. Very handy and supports reusable code.

Schema

module.exports = (sequelize, DataTypes) => {
var Model = sequelize.define('User', {
first : DataTypes.STRING,
last : DataTypes.STRING,
email : {type: DataTypes.STRING, allowNull: true, unique: true, validate: { isEmail: {msg: "Phone number invalid."} }},
phone : {type: DataTypes.STRING, allowNull: true, unique: true, validate: { len: {args: [7, 20], msg: "Phone number invalid, too short."}, isNumeric: { msg: "not a valid phone number."} }},
password : DataTypes.STRING,
});

Associations

Model.associate = function(models){
this.Companies = this.belongsToMany(models.Company, {through: 'UserCompany'});
};

Hash Password, on password save or update.

Model.beforeSave(async (user, options) => {
let err;
if (user.changed('password')){
let salt, hash
[err, salt] = await to(bcrypt.genSalt(10));
if(err) TE(err.message, true);

[err, hash] = await to(bcrypt.hash(user.password, salt));
if(err) TE(err.message, true);

user.password = hash;
}
});

Compare Password

Model.prototype.comparePassword = async function (pw) {
let err, pass
if(!this.password) TE('password not set');

[err, pass] = await to(bcrypt_p.compare(pw, this.password));
if(err) TE(err);

if(!pass) TE('invalid password');

return this;
}

Get JSON Web Token(JWT) for Authentication

Model.prototype.getJWT = function () {
let expiration_time = parseInt(CONFIG.jwt_expiration);
return "Bearer "+jwt.sign({user_id:this.id}, CONFIG.jwt_encryption, {expiresIn: expiration_time});
};

Return Model

    return Model;
};

Company Model

models/company.js

const {TE, to}              = require('../services/util.service');

module.exports = (sequelize, DataTypes) => {
var Model = sequelize.define('Company', {
name: DataTypes.STRING
});

Model.associate = function(models){
this.Users = this.belongsToMany(models.User, {through: 'UserCompany'});
};

Model.prototype.toWeb = function (pw) {
let json = this.toJSON();
return json;
};

return Model;
};

Now lets move to Routing our App

routes/v1.js

import modules and setup passport middleware

const express         = require('express');
const router = express.Router();

const UserController = require('../controllers/user.controller');
const CompanyController = require('../controllers/company.controller');
const HomeController = require('../controllers/home.controller');

const custom = require('./../middleware/custom');

const passport = require('passport');
const path = require('path');

Basic CRUD(create, read, update, delete) routes. You can test these routes using postman or curl. In app.js we set it up with versioning. So to make a request to these routes you must use /v1/{route}. example

url: localhost:3000/v1/users

User Routes

router.post('/users', UserController.create); //create   

router.get('/users',passport.authenticate('jwt', {session:false}), UserController.get); //read

router.put('/users',passport.authenticate('jwt', {session:false}), UserController.update); //update

router.delete('/users',passport.authenticate('jwt',{session:false}), UserController.remove); //delete
router.post( '/users/login', UserController.login);

Company Routes

router.post(    '/companies',           
passport.authenticate('jwt', {session:false}), CompanyController.create);
router.get( '/companies', passport.authenticate('jwt', {session:false}), CompanyController.getAll);

router.get( '/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.get);
router.put( '/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.update);router.delete( '/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.remove);

Export Routes:

module.exports = router;

Lets look at the middleware that was quickly skipped over. (repeat code);

require('./../middleware/passport')(passport)

middleware/passport.js

require modules

const { ExtractJwt, Strategy } = require('passport-jwt');
const { User } = require('../models');
const CONFIG = require('../config/config');
const {to} = require('../services/util.service');

This is what defines our user to all of our routes using the passport middleware. We store the user id in the token. It is then included in the header as Authorization: Bearer a23uiabsdkjd….

This middleware reads the token for the user id and then grabs the user and sends it to our controllers. I know this may seem complicated at first. But using Postman to test this will quickly make it make sense.

module.exports = function(passport){
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = CONFIG.jwt_encryption;

passport.use(new Strategy(opts, async function(jwt_payload, done){
let err, user;
[err, user] = await to(User.findById(jwt_payload.user_id));

if(err) return done(err, false);
if(user) {
return done(null, user);
}else{
return done(null, false);
}
}));
}

Custom Middleware

middleware/custom.js

const Company             = require('./../models').Company;
const { to, ReE, ReS } = require('../services/util.service');

let company = async function (req, res, next) {
let company_id, err, company;
company_id = req.params.company_id;

[err, company] = await to(Company.findOne({where:{id:company_id}}));
if(err) return ReE(res, "err finding company");

if(!company) return ReE(res, "Company not found with id: "+company_id);
let user, users_array, users;
user = req.user;
[err, users] = await to(company.getUsers());

users_array = users.map(obj=>String(obj.user));

if(!users_array.includes(String(user._id))) return ReE(res, "User does not have permission to read app with id: "+app_id);

req.company = company;
next();
}
module.exports.company = company;

Now lets look at our Controllers.

controllers/user.controller.js

require modules

const { User }          = require('../models');
const authService = require('../services/auth.service');
const { to, ReE, ReS } = require('../services/util.service');

Create

Remember the ReE is a helper function that makes all our Error responses have the same format. This way a lazy programmer can not really mess up the way the response will look. This uses a service to actually create the user. This way our controllers stay small. WHICH IS GOOD!

const create = async function(req, res){
res.setHeader('Content-Type', 'application/json');
const body = req.body;

if(!body.unique_key && !body.email && !body.phone){
return ReE(res, 'Please enter an email or phone number to register.');
} else if(!body.password){
return ReE(res, 'Please enter a password to register.');
}else{
let err, user;

[err, user] = await to(authService.createUser(body));

if(err) return ReE(res, err, 422);
return ReS(res, {message:'Successfully created new user.', user:user.toWeb(), token:user.getJWT()}, 201);
}
}
module.exports.create = create;

Get — Pretty basic speaks for itself

the user is returned in req.user from our passport middleware. Remember to include the token in the HEADER if the request. Authorization: Bearer Jasud2732r…

const get = async function(req, res){
res.setHeader('Content-Type', 'application/json');
let user = req.user;

return ReS(res, {user:user.toWeb()});
}
module.exports.get = get;

Update — Still basic

const update = async function(req, res){
let err, user, data
user = req.user;
data = req.body;
user.set(data);

[err, user] = await to(user.save());
if(err){
if(err.message=='Validation error') err = 'The email address or phone number is already in use';
return ReE(res, err);
}
return ReS(res, {message :'Updated User: '+user.email});
}
module.exports.update = update;

Remove

const remove = async function(req, res){
let user, err;
user = req.user;

[err, user] = await to(user.destroy());
if(err) return ReE(res, 'error occured trying to delete user');

return ReS(res, {message:'Deleted User'}, 204);
}
module.exports.remove = remove;

Login

This returns the token for authentication!

const login = async function(req, res){
const body = req.body;
let err, user;

[err, user] = await to(authService.authUser(req.body));
if(err) return ReE(res, err, 422);

return ReS(res, {token:user.getJWT(), user:user.toWeb()});
}
module.exports.login = login;

Lastly! AuthService

services/auth.service.js

require modules

const { User }      = require('../models');
const validator = require('validator');
const { to, TE } = require('../services/util.service');

We would love if the user can use either an email or phone number. This method helps us combine what ever is sent to a variable called unique_key. Which we will use in the create user function

const getUniqueKeyFromBody = function(body){// this is so they can send in 3 options unique_key, email, or phone and it will work
let unique_key = body.unique_key;
if(typeof unique_key==='undefined'){
if(typeof body.email != 'undefined'){
unique_key = body.email
}else if(typeof body.phone != 'undefined'){
unique_key = body.phone
}else{
unique_key = null;
}
}

return unique_key;
}
module.exports.getUniqueKeyFromBody = getUniqueKeyFromBody;

Create User

This validates what the unique is to see if it is a valid email, or valid phone number. Then saves the user in the database. Pretty chill and pretty simple.

const createUser = async function(userInfo){
let unique_key, auth_info, err;

auth_info={}
auth_info.status='create';

unique_key = getUniqueKeyFromBody(userInfo);
if(!unique_key) TE('An email or phone number was not entered.');

if(validator.isEmail(unique_key)){
auth_info.method = 'email';
userInfo.email = unique_key;

[err, user] = await to(User.create(userInfo));
if(err) TE('user already exists with that email');

return user;

}else if(validator.isMobilePhone(unique_key, 'any')){
auth_info.method = 'phone';
userInfo.phone = unique_key;

[err, user] = await to(User.create(userInfo));
if(err) TE('user already exists with that phone number');

return user;
}else{
TE('A valid email or phone number was not entered.');
}
}
module.exports.createUser = createUser;

Auth User

const authUser = async function(userInfo){//returns token
let unique_key;
let auth_info = {};
auth_info.status = 'login';
unique_key = getUniqueKeyFromBody(userInfo);

if(!unique_key) TE('Please enter an email or phone number to login');


if(!userInfo.password) TE('Please enter a password to login');

let user;
if(validator.isEmail(unique_key)){
auth_info.method='email';

[err, user] = await to(User.findOne({where:{email:unique_key}}));
console.log(err, user, unique_key);
if(err) TE(err.message);

}else if(validator.isMobilePhone(unique_key, 'any')){//checks if only phone number was sent
auth_info.method='phone';

[err, user] = await to(User.findOne({where:{phone:unique_key }}));
if(err) TE(err.message);

}else{
TE('A valid email or phone number was not entered');
}

if(!user) TE('Not registered');

[err, user] = await to(user.comparePassword(userInfo.password));

if(err) TE(err.message);

return user;

}
module.exports.authUser = authUser;

Company Controller

controllers/company.controller.js

This follows the same structure as the User Controller.

Create

const { Company } = require('../models');
const { to, ReE, ReS } = require('../services/util.service');

const create = async function(req, res){
res.setHeader('Content-Type', 'application/json');
let err, company;
let user = req.user;

let company_info = req.body;


[err, company] = await to(Company.create(company_info));
if(err) return ReE(res, err, 422);

company.addUser(user, { through: { status: 'started' }})

[err, company] = await to(company.save());
if(err) return ReE(res, err, 422);

let company_json = company.toWeb();
company_json.users = [{user:user.id}];

return ReS(res,{company:company_json}, 201);
}
module.exports.create = create;

Get All Companies that belong to the user

const getAll = async function(req, res){
res.setHeader('Content-Type', 'application/json');
let user = req.user;
let err, companies;

[err, companies] = await to(user.getCompanies());

let companies_json =[]
for( let i in companies){
let company = companies[i];
let users = await company.getUsers()
let company_info = company.toWeb();
let users_info = []
for (let i in users){
let user = users[i];
// let user_info = user.toJSON();
users_info.push({user:user.id});
}
company_info.users = users_info;
companies_json.push(company_info);
}

return ReS(res, {companies:companies_json});
}
module.exports.getAll = getAll;

Get

const get = function(req, res){
res.setHeader('Content-Type', 'application/json');
let company = req.company;

return ReS(res, {company:company.toWeb()});
}
module.exports.get = get;

Update

const update = async function(req, res){
let err, company, data;
company = req.company;
data = req.body;
company.set(data);

[err, company] = await to(company.save());
if(err){
return ReE(res, err);
}
return ReS(res, {company:company.toWeb()});
}
module.exports.update = update;

Remove

const remove = async function(req, res){
let company, err;
company = req.company;

[err, company] = await to(company.destroy());
if(err) return ReE(res, 'error occured trying to delete the company');

return ReS(res, {message:'Deleted Company'}, 204);
}
module.exports.remove = remove;

There we go that is it.

I know I didn’t go into as much detail as I could. There was a lot to go through and the code does speak for itself. If you have any questions please comment bellow. I will try and respond within the hour as I said.

Here is a front end made in angular 5+, that uses this backend for authentication. https://github.com/brianalois/ng-client

UPDATE: Graphql api’s(created by facebook) are better for 90% of apps than restful api’s. I do recommend learning about graphql. Here is a link to a template I have built for them. Click Here!

— Brian Alois Schardt

--

--