Build a Rest API for Node & Mysql 2018 JWT

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); //deleterouter.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