After my article about “Generating a Pdf with Nodejs and Cloudinary” i got few feedbacks from folks requesting that i write an article on Building RESTful Api’s with NodeJs. So i decided to put this piece with the hope that it will help somebody.
You’ll learn how to build a public bus transportation booking API — Such that Users book tickets for their trips, can see all their bookings and have the right to cancel any booking.
The only impossible journey is the one you never begin. 🙌— Tony Robbins
In this post, I’ll explain how to connect and use PostgreSQL Database to store data. We will also learn how to write some basic [SQL](Structure Query Language) queries. SQL is a standard language for storing, manipulating and retrieving data in databases. To make this simple and so that we can have a better understanding of how SQL works, I’m not going to make use ORM(Object Relational Mapping) in this post. Check out this answer on stackoverflow to get a better understanding of ORM. ORM is basically a technique that allows us to query and manipulate data from the database with little knowledge of SQL. ORM packages expose some methods needed to query and manipulate the data in the database. We have several ORM packages that could be used with our database e.g Sequelize. Instead of using an ORM, we will use PG NodeJS package directly — PG is a NodeJs package for interfacing with the PostgreSQL database. Using PG alone will also give us the opportunity to understand some basic SQL queries as we will be querying and manipulating data in the DB using raw SQL queries.
Although I’ll try to keep things simple. However, having some basic Knowledge about Nodejs before diving into this Tutorial is important, this post assumes that you have Nodejs and Npm installed in your PC, if you don’t kindly follow this guide.
**transportApi**
npm init
on your terminal or command prompt if you’re using a window system - Running npm init
will prompt you with some questions to help set up your projectWhen that is done, you should see package.json
file in your project and it contains basic information about your project.
{
"name": "transportapi",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Beveloper",
"license": "ISC"
}
Installation of all dependencies for the project:
We are going to install the required dependencies needed for this project no fancy external packages e.g Morgan e.t.c.
req.body
property..env
file into [process.env](https://nodejs.org/docs/latest/api/process.html#process_process_env)
Run the following command to install all the above packages
$ npm install --save express pg moment body-parser dotenv jsonwebtoken cors make-runnable bcryptjs @babel/polyfill npm-run-all
$ npm install --save-dev babel-core babel-cli babel-preset-env babel-watch babel-preset-es2015 babel-register
If all went well, you should see something similar to this
You’ll notice that express and moment
etc is under dependencies
, that is because those are needed by the time we deploy our code to production. babel-cli, babel-preset-env and babel-watch etc
are only needed during development.
Install PostgreSQL on your system. If you don’t have it installed on your PC walk through this guide
Set up your project structure using the following format;
TransportApi
|-app
|-controllers
|-db
|-helpers
|-middlewares
|-routes
|-node_modules
|-.babelrc
|-.env
|-env.js
|-package-lock.json
|-package.json
|-server.js
Lets create our DB and name it transportApi
. To create the DB, we can make use of any PostgreSQL client such as POSTICO for Mac Users or PgAdmin4 for window users.
If you need a guide on out to setup PostgrelSQL on your PC checkout this easy to read guide
First, let save our Database URL in the system environment. Your Database URL should be in the following format
postgres://{db_username}:{db_password}@{host}:{port}/{db_name}
e.g postgres://transportUser:[email protected]:5235/transport
If you were unable to set your DB locally, the easy way out is to make use of any PostgreSQL cloud DB such as ElephantSQL. If you use cloud PostgreSQL DB, copy the URL and use it instead.
My port may differ from yours, so ensure you use the correct p.
ENVIRONMENT VARIABLE SETUP
Open the .env
file located in the root directory, copy and paste the codes into it.
#.env
DATABASE_URL=postgres://transportUser:[email protected]:5235/transport
PORT=5253
**_DATABASE_URL_** AND **_PORT_**
) so we can use them later across our project.Next, we need to create transportApi
table in our DB. This is where we will start making use of pg
package with basic SQL query to create the table.
Inside our db folder
create a folder called **dev**
and create three files named dbConnection.js, dbQuery.js and pool.js respectively.
The files should look like this.
pool.js
//db/dev/pool.js
import { Pool } from 'pg';
import dotenv from 'dotenv';
dotenv.config();
const databaseConfig = { connectionString: process.env.DATABASE_URL };
const pool = new Pool(databaseConfig);
export default pool;
Code Steps:
Pool
object from pg
. Check pg documentation to read more on pooling
. We use this to connect to our PostgreSQL Db. Pool is the easiest and common way of using pg. You can also make use of their Client API to connect to the DB.dotenv
from dotenv
and load it using dotenv.config()
- what this does is to search for .env
file in our project and load its content into the system environment so we can use node process.env
to access those variables.Pool
and pass in connectionString
to its constructor. We use process.env.DATABASE_URL
to get DATABASE_URL
variable from the system environment.connect
event and console log connected to the db
//db/dev/dbQuery.js
import pool from './pool';
export default {
/**
* DB Query
* @param {object} req
* @param {object} res
* @returns {object} object
*/
query(quertText, params) {
return new Promise((resolve, reject) => {
pool.query(quertText, params)
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
},
};
Code Steps:
query
that takes in two arguments text
- query text and params
- values required by text
. These two arguments are what is needed to query the DB. The method returns a promise and we will call it in our controller. Click here to read more about JavaScript Promise.//db/dev/dbConnection.js
import pool from './pool';
pool.on('connect', () => {
console.log('connected to the db');
});
/**
* Create User Table
* CREATE TABLE test
(id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
phone VARCHAR(100));
*/
const createUserTable = () => {
const userCreateQuery = `CREATE TABLE IF NOT EXISTS users
(id SERIAL PRIMARY KEY,
email VARCHAR(100) UNIQUE NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
password VARCHAR(100) NOT NULL,
created_on DATE NOT NULL)`;
pool.query(userCreateQuery)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
};
/**
* Create Buses Table
*/
const createBusTable = () => {
const busCreateQuery = `CREATE TABLE IF NOT EXISTS bus
(id SERIAL PRIMARY KEY,
number_plate VARCHAR(100) NOT NULL,
manufacturer VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
year VARCHAR(10) NOT NULL,
capacity integer NOT NULL,
created_on DATE NOT NULL)`;
pool.query(busCreateQuery)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
};
/**
* Create Trip Table
*/
const createTripTable = () => {
const tripCreateQuery = `CREATE TABLE IF NOT EXISTS trip
(id SERIAL PRIMARY KEY,
bus_id INTEGER REFERENCES bus(id) ON DELETE CASCADE,
origin VARCHAR(300) NOT NULL,
destination VARCHAR(300) NOT NULL,
trip_date DATE NOT NULL,
fare float NOT NULL,
status float DEFAULT(1.00),
created_on DATE NOT NULL)`;
pool.query(tripCreateQuery)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
};
/**
* Create Booking Table
*/
const createBookingTable = () => {
const bookingCreateQuery = `CREATE TABLE IF NOT EXISTS booking(id SERIAL,
trip_id INTEGER REFERENCES trip(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
bus_id INTEGER REFERENCES bus(id) ON DELETE CASCADE,
trip_date DATE,
seat_number INTEGER UNIQUE,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
created_on DATE NOT NULL,
PRIMARY KEY (id, trip_id, user_id))`;
pool.query(bookingCreateQuery)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
};
/**
* Drop User Table
*/
const dropUserTable = () => {
const usersDropQuery = 'DROP TABLE IF EXISTS users';
pool.query(usersDropQuery)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
};
/**
* Drop Bus Table
*/
const dropBusTable = () => {
const busDropQuery = 'DROP TABLE IF EXISTS bus';
pool.query(busDropQuery)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
};
/**
* Drop Trip Table
*/
const dropTripTable = () => {
const tripDropQuery = 'DROP TABLE IF EXISTS trip';
pool.query(tripDropQuery)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
};
/**
* Drop Bus Table
*/
const dropBookingTable = () => {
const bookingDropQuery = 'DROP TABLE IF EXISTS booking';
pool.query(bookingDropQuery)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
};
/**
* Create All Tables
*/
const createAllTables = () => {
createUserTable();
createBusTable();
createTripTable();
createBookingTable();
};
/**
* Drop All Tables
*/
const dropAllTables = () => {
dropUserTable();
dropBusTable();
dropTripTable();
dropBookingTable();
};
pool.on('remove', () => {
console.log('client removed');
process.exit(0);
});
export {
createAllTables,
dropAllTables,
};
require('make-runnable');
dbConnection.js
Code Steps:
Pool
object from pg
. Check pg documentation to read more on pooling
. We use this to connect to our PostgreSQL Db. Pool is the easiest and common way of using pg. You can also make use of their Client API to connect to the DB.connect
event and console log connected to the db
createTables()
function, inside the function is a query that creates userTbale, busTable, tripTable and bookingTable
respectively with unique fields.const createUserTable = () => {
const userCreateQuery = `CREATE TABLE IF NOT EXISTS users
(id SERIAL PRIMARY KEY,
email VARCHAR(100) UNIQUE NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
password VARCHAR(100) NOT NULL,
created_on DATE NOT NULL)`;
What the above does is to tell PostgreSQL DB to create users
table if users
table does not exist with fields listed and the same goes for the rest of the tables respectively.
Using the first Table(usersTable) as Case study, we observe the following;
pool
query method with userCreateQuery
as an argument and it returns a promise
.pool.end()
to close pool
connection to the db.dropTables()
- What this does it to delete users
table.DROP TABLE IF EXISTS users
, that drops users table if it exists in the DB.pool.on('remove')
to listened to pool
remove
event and use process.exit(0)
to exit the node process.make-runnable
package - We need this to be able to call and any of our two functions from the terminal.make-runnable
at the end. Remember we’d earlier installed make-runnable
as a project dev-dependency.require
instead of import
, this is because we only want to run db.js
file from the terminal alone and it is not directly part of our project so there is no point in compiling it.createTables
function to create our tables. To do this we need to add a command in our Package.json
file.In your start script remove the test command and add this
"scripts": {
"create-dev-tables": "babel-node ./app/db/dev/dbConnection createAllTables",
"start": "nodemon --watch . --exec babel-node -- server",
"setup": "npm-run-all -p start create-dev-tables"
},
Your package.json should know look like this
Next, we run the command $ **npm run setup**
After running the above, you should see something similar below;
NOTE: Instead of going through the above process, you can also create tables directly either using any PostgreSQL client e.g pgAdmin, POSTICO or using PostgreSQL commands on the terminal.
Before diving to the core project, there’s need to create a few helper methods, that would foster resuability across our project.
Copy the following and paste it inside **_app/helpers/validations.js_**
//app/helpers/validation.js
import env from '../../env';
/**
* isValidEmail helper method
* @param {string} email
* @returns {Boolean} True or False
*/
const isValidEmail = (email) => {
const regEx = /\S+@\S+\.\S+/;
return regEx.test(email);
};
/**
* validatePassword helper method
* @param {string} password
* @returns {Boolean} True or False
*/
const validatePassword = (password) => {
if (password.length <= 5 || password === '') {
return false;
} return true;
};
/**
* isEmpty helper method
* @param {string, integer} input
* @returns {Boolean} True or False
*/
const isEmpty = (input) => {
if (input === undefined || input === '') {
return true;
}
if (input.replace(/\s/g, '').length) {
return false;
} return true;
};
/**
* empty helper method
* @param {string, integer} input
* @returns {Boolean} True or False
*/
const empty = (input) => {
if (input === undefined || input === '') {
return true;
}
};
export {
isValidEmail,
validatePassword,
isEmpty,
empty
};
validations.js
Code Steps:
isValidEmail()
method uses regex(Regular Expression) in validating user’s provided email address.validatePassword
— This method checks if the length of the password provided by a user at the time of signup or signin is less than or equal to 5 characters.isEmpty
method checks if any input provided by a user is undefined or empty.Copy the following and paste it inside **_app/helpers/status.js_**
//app/helpers/status.js
const successMessage = { status: 'success' };
const errorMessage = { status: 'error' };
const status = {
success: 200,
error: 500,
notfound: 404,
unauthorized: 401,
conflict: 409,
created: 201,
bad: 400,
nocontent: 204,
};
const trip_statuses = {
active: 1.00,
cancelled: 2.00,
}
export {
successMessage,
errorMessage,
status,
trip_statuses,
};
status.js
Code Steps:
AUTHORIZATION AND AUTHENTICATION_(JWT)_
As we proceed in thus tutorial our project would have a few routes that need protecting and some user’s that need authorizing. Much like myself at one point, you’re probably wondering how this can be achieved. Thankfully, we have JSON Web Tokens (JWT) (among other things) for that.
JSON Web Token (JWT) is an open standard that defines a compact and self-contained way of securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.
Authorization is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Single sign-on is a feature that widely uses JWT nowadays, because of its small overhead and its ability to be easily used across different domains.
There are some advantages of using JWT for authorization:
In its compact form, JSON Web Tokens consist of three parts separated by dots (.
), which are:
Therefore, a JWT typically looks like the following.
xxxxx.yyyyy.zzzzz
jwt.sign(payload, secretkey, [options, callback])
The first functionjwt.sign()
will generate a JWT token, assign it to a user object, and then return that JWT token so we can pass it where ever we may need. It can be either asynchronous or synchronous depending if a callback is supplied. The payload parameter will be the user object in our case, the secretkey is made up by you, and it can be anything. The callback parameter is where we handle sending our token, and the options parameter will be where can set an expiration time among other things.
Copy the following codes and paste it _app/helpers/validations.js_
//app/helpers/validation.js
import env from '../../env';
/**
* isValidEmail helper method
* @param {string} email
* @returns {Boolean} True or False
*/
const isValidEmail = (email) => {
const regEx = /\S+@\S+\.\S+/;
return regEx.test(email);
};
/**
* validatePassword helper method
* @param {string} password
* @returns {Boolean} True or False
*/
const validatePassword = (password) => {
if (password.length <= 5 || password === '') {
return false;
} return true;
};
/**
* isEmpty helper method
* @param {string, integer} input
* @returns {Boolean} True or False
*/
const isEmpty = (input) => {
if (input === undefined || input === '') {
return true;
}
if (input.replace(/\s/g, '').length) {
return false;
} return true;
};
/**
* empty helper method
* @param {string, integer} input
* @returns {Boolean} True or False
*/
const empty = (input) => {
if (input === undefined || input === '') {
return true;
}
};
export {
isValidEmail,
validatePassword,
isEmpty,
empty
};
Code Steps:
Inside validation.js we have a function generateUserToken
generateUserToken
, we used jwt.sign(..)
in signing user’s token, by sending email, user_id, is_admin(will be discussed later ), first_name and last_name as the payload, we also passed our secret and setting token to expire in 3 days. .jwt.verify()_**
jwt.verify(token, secretkey, [options, callback])
jwt.verify()
will verify the users token when a protected route is accessed. It takes in the token as one parameter, the secret key that you defined in the jwt.sign()
function, and then you have the options and callback parameters. The callback will be where we can access and send protected data.
Copy the following and paste it _app/middleware/verifyAuth.js_
//app/middleware/verifyAuth.js
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import {
errorMessage, status,
} from '../helpers/status';
import env from '../../env';
dotenv.config();
/**
* Verify Token
* @param {object} req
* @param {object} res
* @param {object} next
* @returns {object|void} response object
*/
const verifyToken = async (req, res, next) => {
const { token } = req.headers;
if (!token) {
errorMessage.error = 'Token not provided';
return res.status(status.bad).send(errorMessage);
}
try {
const decoded = jwt.verify(token, process.env.SECRET);
req.user = {
email: decoded.email,
user_id: decoded.user_id,
is_admin: decoded.is_admin,
first_name: decoded.first_name,
last_name: decoded.last_name,
};
next();
} catch (error) {
errorMessage.error = 'Authentication Failed';
return res.status(status.unauthorized).send(errorMessage);
}
};
export default verifyToken;
verifyAuth.js
Code Steps:
Here, we create a new Auth object with verifyToken()
method. What verifyToken()
method does is basically to validate and decode user request token using the same secret key we used in signing the token. We used **const{token} = req.headers**
to get the token from the request header and send it to jwt.verify(..)
along with the secret we used in signing the token. If the token is valid, we retrieve **_email, user_id, is_admin,first_name, last_name_**
from the token payload and query the DB to make sure the user exists in the DB. If the user exists in the DB, we created a new object property in the req
object. We will use this to process other requests handler. Finally, since this method is a middleware, we used next()
in moving to the next request handler. If any error occurred in here, we return an error message back to the user without having to move to the next request handler.
Next, we need to setup our controllers for our projects and starting with is our admin and User controller.
Copy the following and paste it inside **_app/controllers/adminController.js_**
//app/controller/adminController.js
import moment from 'moment';
import dbQuery from '../db/dev/dbQuery';
import {
hashPassword,
isValidEmail,
validatePassword,
isEmpty,
generateUserToken,
} from '../helpers/validations';
import {
errorMessage, successMessage, status,
} from '../helpers/status';
/**
* Create A Admin
* @param {object} req
* @param {object} res
* @returns {object} reflection object
*/
const createAdmin = async (req, res) => {
const {
email, first_name, last_name, password,
} = req.body;
const { is_admin } = req.user;
const isAdmin = true;
const created_on = moment(new Date());
if (!is_admin === false) {
errorMessage.error = 'Sorry You are unauthorized to create an admin';
return res.status(status.bad).send(errorMessage);
}
if (isEmpty(email) || isEmpty(first_name) || isEmpty(last_name) || isEmpty(password)) {
errorMessage.error = 'Email, password, first name and last name field cannot be empty';
return res.status(status.bad).send(errorMessage);
}
if (!isValidEmail(email)) {
errorMessage.error = 'Please enter a valid Email';
return res.status(status.bad).send(errorMessage);
}
if (!validatePassword(password)) {
errorMessage.error = 'Password must be more than five(5) characters';
return res.status(status.bad).send(errorMessage);
}
const hashedPassword = hashPassword(password);
const createUserQuery = `INSERT INTO
users(email, first_name, last_name, password, is_admin, created_on)
VALUES($1, $2, $3, $4, $5, $6)
returning *`;
const values = [
email,
first_name,
last_name,
hashedPassword,
isAdmin,
created_on,
];
try {
const { rows } = await dbQuery.query(createUserQuery, values);
const dbResponse = rows[0];
delete dbResponse.password;
const token = generateUserToken(dbResponse.email, dbResponse.id, dbResponse.is_admin, dbResponse.first_name, dbResponse.last_name);
successMessage.data = dbResponse;
successMessage.data.token = token;
return res.status(status.created).send(successMessage);
} catch (error) {
if (error.routine === '_bt_check_unique') {
errorMessage.error = 'Admin with that EMAIL already exist';
return res.status(status.conflict).send(errorMessage);
}
}
};
/**
* Update A User to Admin
* @param {object} req
* @param {object} res
* @returns {object} updated user
*/
const updateUserToAdmin = async (req, res) => {
const { id } = req.params;
const { isAdmin } = req.body;
const { is_admin } = req.user;
if (!is_admin === true) {
errorMessage.error = 'Sorry You are unauthorized to make a user an admin';
return res.status(status.bad).send(errorMessage);
}
if (isAdmin === '') {
errorMessage.error = 'Admin Status is needed';
return res.status(status.bad).send(errorMessage);
}
const findUserQuery = 'SELECT * FROM users WHERE id=$1';
const updateUser = `UPDATE users
SET is_admin=$1 WHERE id=$2 returning *`;
try {
const { rows } = await dbQuery.query(findUserQuery, [id]);
const dbResponse = rows[0];
if (!dbResponse) {
errorMessage.error = 'User Cannot be found';
return res.status(status.notfound).send(errorMessage);
}
const values = [
isAdmin,
id,
];
const response = await dbQuery.query(updateUser, values);
const dbResult = response.rows[0];
delete dbResult.password;
successMessage.data = dbResult;
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'Operation was not successful';
return res.status(status.error).send(errorMessage);
}
};
export {
createAdmin,
updateUserToAdmin,
};
adminController.js
Code Steps:
Note: There are a few routes that needs an admin privilege to access
createAdmin
and updateUserToAdmin
. We also made use of async/await
.app/db/dev/dbQuery.js
.empty
and errorMessage,successMessage, status
from app/helpers/status.js
.hashPassword
and comparePassword, isValidEmail, validPassword, isEmpty, generateUserToken
from app/helpers/validations.js
.createAdmin
, in it we deconstructed our values email, first_name, last_name, is_admin, password
from the request body.**_isValidEmail._**
hashPassword
, we hash our password using the bcryptjs module.Copy the following and paste it inside **_app/controllers/usersController.js_**
//app/controller/usersController.js
import moment from 'moment';
import dbQuery from '../db/dev/dbQuery';
import {
hashPassword,
comparePassword,
isValidEmail,
validatePassword,
isEmpty,
generateUserToken,
} from '../helpers/validations';
import {
errorMessage, successMessage, status,
} from '../helpers/status';
/**
* Create A User
* @param {object} req
* @param {object} res
* @returns {object} reflection object
*/
const createUser = async (req, res) => {
const {
email, first_name, last_name, password,
} = req.body;
const created_on = moment(new Date());
if (isEmpty(email) || isEmpty(first_name) || isEmpty(last_name) || isEmpty(password)) {
errorMessage.error = 'Email, password, first name and last name field cannot be empty';
return res.status(status.bad).send(errorMessage);
}
if (!isValidEmail(email)) {
errorMessage.error = 'Please enter a valid Email';
return res.status(status.bad).send(errorMessage);
}
if (!validatePassword(password)) {
errorMessage.error = 'Password must be more than five(5) characters';
return res.status(status.bad).send(errorMessage);
}
const hashedPassword = hashPassword(password);
const createUserQuery = `INSERT INTO
users(email, first_name, last_name, password, created_on)
VALUES($1, $2, $3, $4, $5)
returning *`;
const values = [
email,
first_name,
last_name,
hashedPassword,
created_on,
];
try {
const { rows } = await dbQuery.query(createUserQuery, values);
const dbResponse = rows[0];
delete dbResponse.password;
const token = generateUserToken(dbResponse.email, dbResponse.id, dbResponse.is_admin, dbResponse.first_name, dbResponse.last_name);
successMessage.data = dbResponse;
successMessage.data.token = token;
return res.status(status.created).send(successMessage);
} catch (error) {
if (error.routine === '_bt_check_unique') {
errorMessage.error = 'User with that EMAIL already exist';
return res.status(status.conflict).send(errorMessage);
}
errorMessage.error = 'Operation was not successful';
return res.status(status.error).send(errorMessage);
}
};
/**
* Signin
* @param {object} req
* @param {object} res
* @returns {object} user object
*/
const siginUser = async (req, res) => {
const { email, password } = req.body;
if (isEmpty(email) || isEmpty(password)) {
errorMessage.error = 'Email or Password detail is missing';
return res.status(status.bad).send(errorMessage);
}
if (!isValidEmail(email) || !validatePassword(password)) {
errorMessage.error = 'Please enter a valid Email or Password';
return res.status(status.bad).send(errorMessage);
}
const signinUserQuery = 'SELECT * FROM users WHERE email = $1';
try {
const { rows } = await dbQuery.query(signinUserQuery, [email]);
const dbResponse = rows[0];
if (!dbResponse) {
errorMessage.error = 'User with this email does not exist';
return res.status(status.notfound).send(errorMessage);
}
if (!comparePassword(dbResponse.password, password)) {
errorMessage.error = 'The password you provided is incorrect';
return res.status(status.bad).send(errorMessage);
}
const token = generateUserToken(dbResponse.email, dbResponse.id, dbResponse.is_admin, dbResponse.first_name, dbResponse.last_name);
delete dbResponse.password;
successMessage.data = dbResponse;
successMessage.data.token = token;
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'Operation was not successful';
return res.status(status.error).send(errorMessage);
}
};
export {
createUser,
siginUser,
};
usersController.js
Code Steps:
createUser
and signUser
. We also made use of async/await
.app/db/dev/dbQuery.js
.empty
and errorMessage,successMessage, status
from app/helpers/status.js
.hashPassword
and comparePassword, isValidEmail, validPassword, isEmpty, generateUserToken
from app/helpers/validations.js
.createUser
, in it we deconstructed our values email, first_name, last_name, password
from the request body.**_isValidEmail._**
hashPassword
, we hash our password using the bcryptjs module.INSERT INTO users(list_columns_here..) VALUES($1, $2, $3, $4, $5...)
- what this does is create a new row in users
table and insert the supplied values into its fields. values
is an array of values that contains what we want to insert into the table. The elements inside values
array must be in the same order as $1, $2, $3, $4, $5
. We used returning *
to return the created row. Remembered we created query
method that takes in two arguments quertText
and params
inside app/db/dev/dbQuery.js
, this is where we will use it. We called the method and send in createUserQuery
and values as parameters.**__bt_check_unique_**
— Checks for duplicate values and returns an error.Since dbQuery.query
returns a promise we make use of async/await
to make our code looks sexy😘.
**_signUser_**
- We set up SELECT * FROM users WHERE email = $1
to get a user with a specific email**.**createUser and signUser
respectively, so we can use them across our project.Copy the following and paste it inside **_app/controllers/busController.js_**
//app/controllers/busControllers.js
import moment from 'moment';
import dbQuery from '../db/dev/dbQuery';
import {
empty,
} from '../helpers/validations';
import {
errorMessage, successMessage, status,
} from '../helpers/status';
/**
* Add A Bus
* @param {object} req
* @param {object} res
* @returns {object} reflection object
*/
const addBusDetails = async (req, res) => {
const {
number_plate, manufacturer, model, year, capacity,
} = req.body;
const created_on = moment(new Date());
if (empty(number_plate) || empty(manufacturer) || empty(model) || empty(year)
|| empty(capacity)) {
errorMessage.error = 'All fields are required';
return res.status(status.bad).send(errorMessage);
}
const createBusQuery = `INSERT INTO
bus(number_plate, manufacturer, model, year, capacity, created_on)
VALUES($1, $2, $3, $4, $5, $6)
returning *`;
const values = [
number_plate,
manufacturer,
model,
year,
capacity,
created_on,
];
try {
const { rows } = await dbQuery.query(createBusQuery, values);
const dbResponse = rows[0];
successMessage.data = dbResponse;
return res.status(status.created).send(successMessage);
} catch (error) {
errorMessage.error = 'Unable to add bus';
return res.status(status.error).send(errorMessage);
}
};
/**
* Get All Buses
* @param {object} req
* @param {object} res
* @returns {object} buses array
*/
const getAllBuses = async (req, res) => {
const getAllBusQuery = 'SELECT * FROM bus ORDER BY id DESC';
try {
const { rows } = await dbQuery.query(getAllBusQuery);
const dbResponse = rows;
if (dbResponse[0] === undefined) {
errorMessage.error = 'There are no buses';
return res.status(status.notfound).send(errorMessage);
}
successMessage.data = dbResponse;
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'An error Occured';
return res.status(status.error).send(errorMessage);
}
};
export {
addBusDetails,
getAllBuses,
};
busController.js
Code Steps:
addBusDetails()
and getAllBuses()
. We also made use of async/await
.addbusDetails()
- We imported our dbQuery from app/db/dev/dbQuery.js
.empty
and errorMessage,successMessage, status
from app/helpers/validations.js
. and app/helpers/status.js
respectively.addBusDetails()
, in it we deconstructed our values number_plate, manufacturer, model, year, capacity
from the request body.**_empty_**
method, if its empty it sends an error “all fields are required” and returns a status code of 400.INSERT INTO bus(list_columns_here..) VALUES($1, $2, $3, $4, $5, $6 ...)
- what this does is create a new row in bus
table and insert the supplied values into its fields. values
is an array of values that contains what we want to insert into the table. The elements inside values
array must be in the same order as $1, $2, $3, $4, $5, $6
. We used returning *
to return the created row. Remembered we created query
method that takes in two arguments quertText
and params
inside app/db/dev/dbQuery.js
, this is where we will use it. We called the method and send in createBusQuery
and values as parameters. Since dbQuery.query
returns a promise we make use of async/await
to make our code looks sexy😘.
Next, we’d be creating our tripController, similar to the bus Controller.
Copy the following and paste it inside **_app/controllers/tripController.js_**
// app/controllers/tripController.js
import moment from 'moment';
import dbQuery from '../db/dev/dbQuery';
import {
isEmpty, empty,
} from '../helpers/validations';
import {
errorMessage, successMessage, status, trip_statuses,
} from '../helpers/status';
/**
* Create A Trip
* @param {object} req
* @param {object} res
* @returns {object} reflection object
*/
const createTrip = async (req, res) => {
const {
bus_id, origin, destination, trip_date, fare,
} = req.body;
const { is_admin } = req.user;
if (!is_admin === true) {
errorMessage.error = 'Sorry You are unauthorized to create a trip';
return res.status(status.bad).send(errorMessage);
}
const created_on = moment(new Date());
if (empty(bus_id) || isEmpty(origin) || isEmpty(destination) || empty(trip_date) || empty(fare)) {
errorMessage.error = 'Origin, Destination, Trip Date and Fare, field cannot be empty';
return res.status(status.bad).send(errorMessage);
}
const createTripQuery = `INSERT INTO
trip(bus_id, origin, destination, trip_date, fare, created_on)
VALUES($1, $2, $3, $4, $5, $6)
returning *`;
const values = [
bus_id,
origin,
destination,
trip_date,
fare,
created_on,
];
try {
const { rows } = await dbQuery.query(createTripQuery, values);
const dbResponse = rows[0];
successMessage.data = dbResponse;
return res.status(status.created).send(successMessage);
} catch (error) {
errorMessage.error = 'Unable to create trip';
return res.status(status.error).send(errorMessage);
}
};
/**
* Get All Trips
* @param {object} req
* @param {object} res
* @returns {object} trips array
*/
const getAllTrips = async (req, res) => {
const getAllTripsQuery = 'SELECT * FROM trip ORDER BY id DESC';
try {
const { rows } = await dbQuery.query(getAllTripsQuery);
const dbResponse = rows;
if (!dbResponse[0]) {
errorMessage.error = 'There are no trips';
return res.status(status.notfound).send(errorMessage);
}
successMessage.data = dbResponse;
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'Operation was not successful';
return res.status(status.error).send(errorMessage);
}
};
/**
* cancel A Trip
* @param {object} req
* @param {object} res
* @returns {void} return Trip cancelled successfully
*/
const cancelTrip = async (req, res) => {
const { tripId } = req.params;
const { is_admin } = req.user;
const { cancelled } = trip_statuses;
if (!is_admin === true) {
errorMessage.error = 'Sorry You are unauthorized to cancel a trip';
return res.status(status.bad).send(errorMessage);
}
const cancelTripQuery = 'UPDATE trip SET status=$1 WHERE id=$2 returning *';
const values = [
cancelled,
tripId,
];
try {
const { rows } = await dbQuery.query(cancelTripQuery, values);
const dbResponse = rows[0];
if (!dbResponse) {
errorMessage.error = 'There is no trip with that id';
return res.status(status.notfound).send(errorMessage);
}
successMessage.data = {};
successMessage.data.message = 'Trip cancelled successfully';
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'Operation was not successful';
return res.status(status.error).send(errorMessage);
}
};
/**
* filter trips by origin
* @param {object} req
* @param {object} res
* @returns {object} returned trips
*/
const filterTripByOrigin = async (req, res) => {
const { origin } = req.query;
const findTripQuery = 'SELECT * FROM trip WHERE origin=$1 ORDER BY id DESC';
try {
const { rows } = await dbQuery.query(findTripQuery, [origin]);
const dbResponse = rows;
if (!dbResponse[0]) {
errorMessage.error = 'No Trips with that origin';
return res.status(status.notfound).send(errorMessage);
}
successMessage.data = dbResponse;
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'Operation was not successful';
return res.status(status.error).send(errorMessage);
}
};
/**
* filter trips by destination
* @param {object} req
* @param {object} res
* @returns {object} returned trips
*/
const filterTripByDestination = async (req, res) => {
const { destination } = req.query;
const findTripQuery = 'SELECT * FROM trip WHERE destination=$1 ORDER BY id DESC';
try {
const { rows } = await dbQuery.query(findTripQuery, [destination]);
const dbResponse = rows;
if (!dbResponse[0]) {
errorMessage.error = 'No Trips with that destination';
return res.status(status.notfound).send(errorMessage);
}
successMessage.data = dbResponse;
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'Operation was not successful';
return res.status(status.error).send(errorMessage);
}
};
export {
createTrip,
getAllTrips,
cancelTrip,
filterTripByOrigin,
filterTripByDestination,
};
tripController.js
Code Steps:
The tripController is almost similar to that of the BusController earlier created, but with few additions
createTrip(), getAllTrips()cancelTrips, filterTripByOrigin, filterTripByDestination
.UPDATE trip SET status=$1 WHERE id=$2 returning*
to cancel trips according to trip ID.SELECT * FROM trip WHERE **_origin_**=$1 ORDER BY id DESC
to filter the origin of a users trip from the descending order .SELECT * FROM trip WHERE destination=$1 ORDER BY id DESC
to filter the destination of a user trip from the descending order .Next, we’d consider the bookingController which seems to be an important part of our transportApi.
Copy the following and paste it inside **_app/controllers/bookingController.js_**
//app/controllers/bookingController.js
import moment from 'moment';
import dbQuery from '../db/dev/dbQuery';
import {
empty,
} from '../helpers/validations';
import {
errorMessage, successMessage, status,
} from '../helpers/status';
/**
* Add A Booking
* @param {object} req
* @param {object} res
* @returns {object} reflection object
*/
const createBooking = async (req, res) => {
const {
trip_id, bus_id, trip_date, seat_number,
} = req.body;
const {
first_name, last_name, user_id, email,
} = req.user;
const created_on = moment(new Date());
if (empty(trip_id)) {
errorMessage.error = 'Trip is required';
return res.status(status.bad).send(errorMessage);
}
const createBookingQuery = `INSERT INTO
booking(user_id, trip_id, bus_id, trip_date, seat_number, first_name, last_name, email, created_on)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)
returning *`;
const values = [
user_id,
trip_id,
bus_id,
trip_date,
seat_number,
first_name,
last_name,
email,
created_on,
];
try {
const { rows } = await dbQuery.query(createBookingQuery, values);
const dbResponse = rows[0];
successMessage.data = dbResponse;
return res.status(status.created).send(successMessage);
} catch (error) {
if (error.routine === '_bt_check_unique') {
errorMessage.error = 'Seat Number is taken already';
return res.status(status.conflict).send(errorMessage);
}
errorMessage.error = 'Unable to create booking';
return res.status(status.error).send(errorMessage);
}
};
/**
* Get All Bookings
* @param {object} req
* @param {object} res
* @returns {object} buses array
*/
const getAllBookings = async (req, res) => {
const { is_admin, user_id } = req.user;
if (!is_admin === true) {
const getAllBookingsQuery = 'SELECT * FROM booking WHERE user_id = $1';
try {
const { rows } = await dbQuery.query(getAllBookingsQuery, [user_id]);
const dbResponse = rows;
if (dbResponse[0] === undefined) {
errorMessage.error = 'You have no bookings';
return res.status(status.notfound).send(errorMessage);
}
successMessage.data = dbResponse;
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'An error Occured';
return res.status(status.error).send(errorMessage);
}
}
const getAllBookingsQuery = 'SELECT * FROM booking ORDER BY id DESC';
try {
const { rows } = await dbQuery.query(getAllBookingsQuery);
const dbResponse = rows;
if (dbResponse[0] === undefined) {
errorMessage.error = 'There are no bookings';
return res.status(status.bad).send(errorMessage);
}
successMessage.data = dbResponse;
return res.status(status.success).send(successMessage);
} catch (error) {
errorMessage.error = 'An error Occured';
return res.status(status.error).send(errorMessage);
}
};
/**
* Delete A Booking
* @param {object} req
* @param {object} res
* @returns {void} return response booking deleted successfully
*/
const deleteBooking = async (req, res) => {
const { bookingId } = req.params;
const { user_id } = req.user;
const deleteBookingQuery = 'DELETE FROM booking WHERE id=$1 AND user_id = $2 returning *';
try {
const { rows } = await dbQuery.query(deleteBookingQuery, [bookingId, user_id]);
const dbResponse = rows[0];
if (!dbResponse) {
errorMessage.error = 'You have no booking with that id';
return res.status(status.notfound).send(errorMessage);
}
successMessage.data = {};
successMessage.data.message = 'Booking deleted successfully';
return res.status(status.success).send(successMessage);
} catch (error) {
return res.status(status.error).send(error);
}
};
/**
* Update A User to Admin
* @param {object} req
* @param {object} res
* @returns {object} updated user
*/
const updateBookingSeat = async (req, res) => {
const { bookingId } = req.params;
const { seat_number } = req.body;
const { user_id } = req.user;
if (empty(seat_number)) {
errorMessage.error = 'Seat Number is needed';
return res.status(status.bad).send(errorMessage);
}
const findBookingQuery = 'SELECT * FROM booking WHERE id=$1';
const updateBooking = `UPDATE booking
SET seat_number=$1 WHERE user_id=$2 AND id=$3 returning *`;
try {
const { rows } = await dbQuery.query(findBookingQuery, [bookingId]);
const dbResponse = rows[0];
if (!dbResponse) {
errorMessage.error = 'Booking Cannot be found';
return res.status(status.notfound).send(errorMessage);
}
const values = [
seat_number,
user_id,
bookingId,
];
const response = await dbQuery.query(updateBooking, values);
const dbResult = response.rows[0];
delete dbResult.password;
successMessage.data = dbResult;
return res.status(status.success).send(successMessage);
} catch (error) {
if (error.routine === '_bt_check_unique') {
errorMessage.error = 'Seat Number is taken already';
return res.status(status.conflict).send(errorMessage);
}
errorMessage.error = 'Operation was not successful';
return res.status(status.error).send(errorMessage);
}
};
export {
createBooking,
getAllBookings,
deleteBooking,
updateBookingSeat,
};
bookingController.js
Code steps:
The bookingController is almost similar to that of the BusController earlier created, but with few additions
createBooking(), getAllBooking, deleteBooking, and updateBookingSeat
.UPDATE trip SET status=$1 WHERE id=$2 returning*
to cancel trips according to trip ID.UPDATE booking SET seat_number=$1 WHERE user_id=$2 returning*
to update bookings according to the users ID and the seat number of a user, during booking.If you have gotten this far, you deserve a thumbs up!
Next, we’d consider setting up our routes for every individual controller created.
Copy the following and paste it in **_app/routes/busRoute.js._**
//app/routes/busRoute.js
import express from 'express';
import { addBusDetails, getAllBuses } from '../controllers/busController';
import verifyAuth from '../middlewares/verifyAuth';
const router = express.Router();
// buses Routes
router.post('/buses', verifyAuth, addBusDetails);
router.get('/buses', verifyAuth, getAllBuses);
export default router;
busRoute.js
Code Steps:
**_app/controllers/busController.js._**
**_router.post('/buses', addBusDetails), router.get('/buses', getAllBuses)etc_**
.**verifyAuth**
middleware before processing the actual request. With this new setup, a user without a valid token will get an error.//app/routes/bookingRoute.js
import express from 'express';
import { createBooking, getAllBookings, deleteBooking, updateBookingSeat } from '../controllers/bookingController';
import verifyAuth from '../middlewares/verifyAuth';
const router = express.Router();
// bookings Routes
router.post('/bookings', verifyAuth, createBooking);
router.get('/bookings', verifyAuth, getAllBookings);
router.delete('/bookings/:bookingId', verifyAuth, deleteBooking);
router.put('/bookings/:bookingId', verifyAuth, updateBookingSeat);
export default router;
bookingRoute.js
//app/routes/tripRoute.js
import express from 'express';
import { createTrip, getAllTrips, cancelTrip, filterTripByOrigin, filterTripByDestination } from '../controllers/tripController';
import verifyAuth from '../middlewares/verifyAuth';
const router = express.Router();
// trips Routes
router.post('/trips', verifyAuth, createTrip);
router.get('/trips', verifyAuth, getAllTrips);
router.patch('/trips/:tripId', verifyAuth, cancelTrip);
router.get('/trips/origin', verifyAuth, filterTripByOrigin);
router.get('/trips/destinatiovan', verifyAuth, filterTripByDestination);
export default router;
tripRoute.js
Now that we’ve our Controller and Route setup, lets see how to put everything together.
Copy the following codes and paste it in **_app/server.js._**
//server.js
import express from 'express';
import 'babel-polyfill';
import cors from 'cors';
import env from './env';
import usersRoute from './app/routes/usersRoute';
import seedRoute from './app/routes/seedRoute';
import adminRoute from './app/routes/adminRoute';
import tripRoute from './app/routes/tripRoute';
import busRoute from './app/routes/busRoute';
import bookingRoute from './app/routes/bookingRoute';
import familyRoute from './app/routes/familyRoute';
const app = express();
// Add middleware for parsing URL encoded bodies (which are usually sent by browser)
app.use(cors());
// Add middleware for parsing JSON and urlencoded data and populating `req.body`
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use('/api/v1', usersRoute);
app.use('/api/v1', familyRoute);
app.use('/api/v1', seedRoute);
app.use('/api/v1', adminRoute);
app.use('/api/v1', tripRoute);
app.use('/api/v1', busRoute);
app.use('/api/v1', bookingRoute);
app.listen(env.port).on('listening', () => {
console.log(`🚀 are live on ${env.port}`);
});
export default app;
server.js
Code Steps:
**.**We installed babel-polyfill npm package and imported it — We need this here so that node runtime will recognize async/await
and Promise
.
**.**We imported the necessary packages from their respective modules
**.**Next, we imported all the routes from our routes folder
**.**Adding cors middleware for parsing URL encoded bodies (which are usually sent by browser)
.Express.urlencoded is added as a middleware for parsing JSON and urlencoded data and populating req.body
.
**.**Next, we versioned our Api **_'/api/v1', userRoute_**
and appended the respective routes.
**.**Lastly we listened to our port so we can start the server.
Copy Token and put it in the Header
We just successfully built our Transport Backend App.
If you came thus far congrats, ✌.
Surely, you’ve got questions or issues as you went through this tutorial, kindly drop you comments and you’d be responded to ASAP.
Once again thanks for reading, Clap, and share!👌
Checkout the complete code here
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Learn JavaScript - Become a Zero to Hero
☞ Javascript Project Tutorial: Budget App
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch