https://codeburst.io/build-a-rest-api-for-node-mysql-2018-jwt-6957bcfc7ac9


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.

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+, and React), 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.

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

Main file app.js

Here I am instantiating the global variables, and global functions. I will go over these files later

require('./config/config');     
require('./global_functions');

Require dependencies, and instantiate server.

const express      = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const passport = require('passport');
const v1 = require('./routes/v1');

const app = express();

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');
})
.catch(err => {
console.error('Unable to connect to SQL database:', err);
});
if(CONFIG.app==='development'){
models.sequelize.sync();//creates tables from models
// models.sequelize.sync({ force: true });//good for testing
}

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

app.use(function (req, res, next) {
// Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', '*');
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
// Request headers you wish to allow
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Authorization, Content-Type');
// Set to true if you need the website to include cookies in the requests sent
// to the API (e.g. in case you use sessions)
res.setHeader('Access-Control-Allow-Credentials', true);
// Pass to next layer of middleware
next();
});

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;

config/config.js

This file that was required in app.js was loaded first. It sets up the global variable CONFIG in your app so you can use it any where without requiring it. Super handy!

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

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

CONFIG.app = process.env.APP || 'development';
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';

global_functions.js

These are helper functions that are scoped globally so we can also use them anywhere in our app without requiring them. 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.

to = function(promise) {
return promise
.then(data => {
return [null, data];
}).catch(err =>
[pe(err)]
);
}

pe = require('parse-error');

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

throw new Error(err_message);
}

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});
}

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)
};
//This is here to handle all the uncaught promise rejections
process.on('unhandledRejection', error => {
console.error('Uncaught Error', pe(error));
});

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

models/user.js

import modules

'use strict';
const bcrypt = require('bcrypt');
const bcrypt_p = require('bcrypt-promise');
const jwt = require('jsonwebtoken');

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.

'use strict';
const bcrypt = require('bcrypt');
const bcrypt_p = require('bcrypt-promise');
const jwt = require('jsonwebtoken');

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,
});

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

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;
}
});

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;
}

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});
};

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

return Model;
};

Company Model

models/company.js

'use strict';
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/UserController');
const CompanyController = require('./../controllers/CompanyController');
const HomeController = require('./../controllers/HomeController');

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 JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const User = require('../models').User;

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 JwtStrategy(opts, async function(jwt_payload, done){
let err, user;
[err, user] = await to(User.findById(jwt_payload.user_id));
console.log('user', 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;

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/UserController.js

require modules

const User          = require('../models').User;
const authService = require('./../services/AuthService');

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/AuthService.js

require modules

const User        = require('./../models').User;
const validator = require('validator');

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/CompanyController.js

This follows the same structure as the User Controller.

Create

const Company = require('../models').Company;

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

I started a company dedicated to using best practices in Node. If you have any questions or want to request any tutorials please comment or message me. We are in Orange County, CA. called Orange Technology.

— Brian Alois