Build a Rest API for Node & Mysql 2018 JWT

January 24, 2018 0 Comments

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.

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?

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!

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.

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

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

APP=dev
PORT=3000

DBDIALECT=mysql
DB
HOST=localhost
DBPORT=3306
DB
NAME=dbNameChange
DBUSER=rootChange
DB
PASSWORD=passwordChange

JWTENCRYPTION=PleaseChange
JWT
EXPIRATION=10000

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

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

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;

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.DBDIALECT || 'mysql';
CONFIG.db
host = process.env.DBHOST || 'localhost';
CONFIG.db
port = process.env.DBPORT || '3306';
CONFIG.db
name = process.env.DBNAME || 'name';
CONFIG.db
user = process.env.DBUSER || 'root';
CONFIG.db
password = process.env.DBPASSWORD || 'db-password';

CONFIG.jwt
encryption = process.env.JWTENCRYPTION || 'jwtpleasechange';
CONFIG.jwt
expiration = process.env.JWTEXPIRATION || '10000';

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

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 senddata = {success:true};

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

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

return res.json(send
data)
};
'use strict';

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

var sequelize = new Sequelize(CONFIG.dbname, CONFIG.dbuser, CONFIG.dbpassword, {dialect:CONFIG.dbdialect, operatorsAliases:false});

connect to sequelize using env variables

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

Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
//One to Many
db.Catalog.belongsTo(db.Company);
db.Company.hasMany(db.Catalog);

//Many to Many
db.User.belongsToMany(db.Company, {through: 'UserCompany'});
db.Company.belongsToMany(db.User, {through: 'UserCompany'});
db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

import modules

'use strict';
const bcrypt = require('bcrypt');
const bcryptp = 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.

module.exports = (sequelize, DataTypes) => {
var User = sequelize.define('User', {
first : DataTypes.STRING,
last : DataTypes.STRING,
email : {type: DataTypes.STRING, allowNull: true, unique: true},
phone : {type: DataTypes.STRING, allowNull: true, unique: true},
password : DataTypes.STRING,

}, {
classMethods: {
associate: function(models) {
// associations can be defined here
}
},
hooks: {
beforeSave: async (user) => {
let err, 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;
},
},
});

User.prototype.comparePassword = async function (pw) {
let err, pass
if(!this.password){
return ['password not set'];
}

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

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

return this;
}

User.prototype.getJWT = function () {
let expirationtime = parseInt(CONFIG.jwtexpiration);
return "Bearer "+jwt.sign({userid:this.id}, CONFIG.jwtencryption, {expiresIn: expirationtime});
}

return User;
};

import modules and setup passport middleware

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

const UserController = require('./../controllers/UserController');
const HomeController = require('./../controllers/HomeController');
const passport = require('passport');
const path = require('path');
require('./../middleware/passport')(passport)

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

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);
module.exports = router;

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

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

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(jwtpayload, done){
let err, user;
[err, user] = await to(User.findById(jwt
payload.userid));
console.log('user', user.id);
if(err) return done(err, false);
if(user) {
return done(null, user);
}else{
return done(null, false);
}
}));
}

require modules

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

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.toJSON(), token:user.getJWT()}, 201);
}
}
module.exports.create = create;

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.toJSON()});
}
module.exports.get = get;
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 '+data.email+' is already taken';

return ReE(res, err);
}
return ReS(res, {message :'Updated User: '+user.email});
}
module.exports.update = update;
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;

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.toJSON()});
}
module.exports.login = login;

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 uniquekey. Which we will use in the create user function

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

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

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, authinfo, err;

auth
info={}
authinfo.status='create';

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

if(validator.isEmail(unique
key)){
authinfo.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(uniquekey, 'any')){
auth
info.method = 'phone';
userInfo.phone = uniquekey;

[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;
const authUser = async function(userInfo){//returns token
let unique
key;
let authinfo = {};
auth
info.status = 'login';
uniquekey = 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(uniquekey)){
auth
info.method='email';

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

}else if(validator.isMobilePhone(uniquekey, '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;

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.

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


Tag cloud