
This post offers a comprehensive how-to for setting up an email verification system with Node.js, Express, PostgreSQL, and Nodemailer. It addresses crucial elements like email delivery, JWT-based token generation, user registration, and account verification through database status updates. Developers looking to incorporate secure email verification into their backend applications will find the tutorial to be very helpful.
node.js, Express.js, Postgress
Every website should include an email verification feature. It will protect us against spammers. Because this is my first blog, I shall do my best. Let’s get started coding.
firstly, start creating an express server, use this command that:
Create Node.js App
mkdir verification-emailcd verification-emailNext, we initialize the Node.js App with a package.json file:
npm init -yWe need to install the necessary modules.
npm install express pg pg-hstore sequelize nodemailer joi env2 compression bcryptjs cors jsonwebtoken
npm install nodemon -dExpress: Express is a minimal and flexible Node.js web application framework.
Postgres: Postgres is a database.
sequelize: sequelize is a database ORM.
Nodemailer: Nodemailer allows us to send email.
Joi: Joi is an object schema description language and validator for JavaScript objects.
env2: It loads environment variables from a .env file.
The package.json file should look like this :
{
"name": "verification-email",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon server"
},
"keywords": [
"nodejs",
"express",
"email",
"verification",
"email-verification"
],
"author": "Ahmed Qeshta",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"compression": "^1.7.4",
"cors": "^2.8.5",
"env2": "^2.2.2",
"express": "^4.18.1",
"joi": "^17.6.0",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.7.5",
"pg": "^8.7.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.20.0"
},
"devDependencies": {
"nodemon": "^2.0.16"
}
}server\index.js
const app = require('./app');
const port = app.get('port');
app.listen(port, () => {
console.log(`server is running on http://localhost:${port}`);
});server\app.js
require('env2')('.env');
const express = require('express');
const compression = require('compression');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const { notFundError, serverError } = require('./error');
const routes = require('./routes');
const app = express();
app.disable('x-powered-by');
app.use([
compression(),
cors(),
cookieParser(),
express.json({ limit: '50mb' }),
express.urlencoded({ extended: false })
]);
app.set('port', process.env.PORT || 8080);
app.use('/api/v1/', routes);
app.use(notFundError);
app.use(serverError);
module.exports = app;
server\routes\index.js
const { Router } = require('express');
const auth = require('./auth');
const routes = Router();
// {domain-name}/api/v1/user
routes.use('/user', auth);
module.exports = routes;server\routes\auth.js
const { Router } = require('express');
const { addUser, verifyUser, getUsers } = require('../controllers');
const user = Router();
// {domain-name}/api/v1/user
user.post('/', addUser);
user.get('/', getUsers);
// {domain-name}/api/v1/user/verify/:id/:token
user.get('/verify/:id/:token', verifyUser);
module.exports = user;server\error\index.js
const notFundError = require('./notFoundError');
const serverError = require('./serverError');
module.exports = { notFundError, serverError };server\error\notFoundError.js
const notFoundError = (_, res) => {
res.status(404).json({ status: 404, message: 'Not Found Page' });
};
module.exports = notFoundError;server\error\serverError.js
const serverError = (error, _, res, next) => {
if (error.status) {
res.status(error.status).json({ status: error.status, message: error.message });
} else {
res.status(500).json({ status: 500, message: 'Server Error' });
}
};
module.exports = serverError;server\controllers\index.js
const { addUser, verifyUser, getUsers } = require('./auth');
module.exports = { addUser, verifyUser, getUsers };server\controllers\auth\index.js
const addUser = require('./addUser');
const getUsers = require('./getUsers');
const verifyUser = require('./verifyUser');
module.exports = { addUser, verifyUser, getUsers };server\controllers\auth\addUser.js
const { CustomError, addUserSchema, generateToken } = require('../../utils');
const { User } = require('../../database');
const { hash } = require('bcryptjs');
const { verifyEmail } = require('../../utils/email/templates/verifyEmail');
const sendEmail = require('../../utils/email');
const addUser = async (req, res, next) => {
try {
const { email, password } = await addUserSchema.validateAsync(req.body,{abortEarly: false});
// Check if the gym already exists
const isExist = await User.findOne({ where: { email }, });
// if it exists, throw an error
if (isExist) {
throw CustomError('Sorry, This Email is already exist', 409);
}
const hashedPassword = await hash(password, 12);
const { id } = await User.create({ email, password: hashedPassword });
const payload = { id, email, };
// Generate the token
const token = await generateToken(payload, { expiresIn: '0.5h', algorithm: 'HS256', });
const html = verifyEmail(`${process.env.BASE_URL}api/v1/user/verify/${id}/${token}`);
sendEmail(email, 'Verify Your Email', html);
res.status(201).json({ message: 'An Email sent to your account please verify', }); } catch (error) {
console.log(error);
if (error.name === 'ValidationError') {
return next(CustomError(error.message, 400));
}
return next(error);
}};
module.exports = addUser;server\controllers\auth\verifyUser.js
const { User } = require('../../database');
const { checkToken, CustomError, paramsValidation } = require('../../utils');
const verifyUser = async ({ params }, res, next) => {
try {
const { id, token } = await paramsValidation.validateAsync(params);
// Check if the gym already exists
const user = await User.findByPk(id);
// if it exists, throw an error
if (!user) throw CustomError('Sorry, Invalid link', 409);
const tokenChecked = await checkToken(token);
if (!tokenChecked) throw CustomError('Sorry, Invalid link', 409);
await user.update({ verified: true });
res.json({ message: 'email verified successfully', });
} catch (error) {
if (error.name === 'ValidationError') {
return next(CustomError(error.message, 400));
}
if (error.name === 'TokenExpiredError') {
return next(CustomError('Sorry This link was Invalid, try Again', 500));
}
return next(error);
}};
module.exports = verifyUser;server\controllers\auth\getUsers.js
const { User } = require('../../database');
const getUsers = async (_, res, next) => {
try {
const user = await User.findAll();
res.json({ message: 'Get All User', user, });
} catch (error) {
return next(error);
}
};
module.exports = getUsers;First thing, create a DataBase as verify_email_db, open your terminal or open SQL shell.
Psql -h localhost -p 5432 -U postgresEnter your password for user postgres:
CREATE DATABASE verify_email_db;.env File
BASE_URL = http://localhost:{your port}/NODE_ENV = development
DB_URL = postgres://postgres:yourpassword@localhost:5432/db_name
TEST_DB_URL = postgres://postgres:yourpassword@localhost:5432/db_name_test
DATABASE_URL = your-prodction-DATABASE_URL
JWT_SECRET = your-jwt_SECRET
MAIL_HOST = smtp.mailtrap.ioMAIL_USER = user_mail
MAIL_PASS = passwor_mailserver\database\config\connection.js
const { Sequelize } = require('sequelize');
require('env2')('.env');
const { NODE_ENV, DB_URL, TEST_DB_URL, DATABASE_URL, DB_BUILD } = process.env;
let dbUrl = '';
let ssl = false;
switch (NODE_ENV) {
case 'development':
dbUrl = DB_URL;
ssl = false;
break;
case 'test':
dbUrl = TEST_DB_URL;
ssl = false;
break;
case 'production':
dbUrl = DATABASE_URL;
ssl = { rejectUnauthorized: false };
break;
default:
throw new Error('NODE_ENV is not set');
}
const sequelize = new Sequelize(dbUrl, {
dialect: 'postgres',
dialectOptions: { ssl, charset: 'utf8' },
logging: false,
});
if (!DB_BUILD) {
// sync sequelize when DB_BUILD equals false
sequelize.sync();
}
module.exports = sequelize;server\database\models\index.js
const User = require('./users');
module.exports = { User };server\database\models\user.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/connection');
const User = sequelize.define('users', {
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, },
email: { type: DataTypes.STRING, allowNull: false, unique: true, },
password: { type: DataTypes.STRING, allowNull: false, },
verified: { type: DataTypes.BOOLEAN, defaultValue: false, }
});
module.exports = User;server\database\index.js
const sequelize = require('./config/connection');
const { User } = require('./models');
module.exports = { User, sequelize,};In the utils folder, create an email folder, then create a configuration, and create a template email.
server\utils\email\index.js
const nodemailer = require('nodemailer');
const { EMAIL_SENDER, MAIL_HOST, MAIL_USER, MAIL_PASS } = process.env;
const sendEmail = async (to, subject, html) => {
try {
const transport = nodemailer.createTransport({
host: MAIL_HOST,
port: 2525,
// secure: true,
auth: { user: MAIL_USER, pass: MAIL_PASS, },
});
await transport.sendMail({ from: EMAIL_SENDER, to, subject, html, });
} catch (error) {
console.log(error);
}
};
module.exports = sendEmail;server\utils\email\templates\verifyEmail.js
const styles = ` <style type="text/css">@media only screen and (min-width: 520px) {.u-row {width: 500px !important;}.u-row .u-col {vertical-align: top;}.u-row .u-col-100 {width: 500px !important;}}@media (max-width: 520px) {.u-row-container {max-width: 100% !important;padding-left: 0px !important;padding-right: 0px !important;}.u-row .u-col {min-width: 320px !important;max-width: 100% !important;display: block !important;}.u-row {width: calc(100% - 40px) !important;}.u-col {width: 100% !important;}.u-col > div {margin: 0 auto;}}body {margin: 0;padding: 0;}table,tr,td {vertical-align: top;border-collapse: collapse;}p {margin: 0;}.ie-container table,.mso-container table {table-layout: fixed;}* {line-height: inherit;}a[x-apple-data-detectors='true'] {color: inherit !important;text-decoration: none !important;}table, td { color: #000000; } a { color: #0000ee; text-decoration: underline; }</style>`;const verifyEmail = (link) => { return ` <!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> <head> <!--[if gte mso 9]> <xml> <o:OfficeDocumentSettings> <o:AllowPNG/> <o:PixelsPerInch>96</o:PixelsPerInch> </o:OfficeDocumentSettings> </xml> <![endif]--> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="x-apple-disable-message-reformatting"> <!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]--> <title></title> ${styles} </head> <body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #e7e7e7;color: #000000"> <!--[if IE]><div class="ie-container"><![endif]--> <!--[if mso]><div class="mso-container"><![endif]--> <table style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #e7e7e7;width:100%" cellpadding="0" cellspacing="0"> <tbody> <tr style="vertical-align: top"> <td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top"> <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #e7e7e7;"><![endif]--> <div class="u-row-container" style="padding: 0px;background-color: transparent"> <div class="u-row" style="Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;"> <div style="border-collapse: collapse;display: table;width: 100%;background-color: transparent;"> <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]--> <!--[if (mso)|(IE)]><td align="center" width="500" style="width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]--> <div class="u-col u-col-100" style="max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;"> <div style="width: 100% !important;"> <!--[if (!mso)&(!IE)]><!--><div style="padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"><!--<![endif]--> <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody> <tr> <td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left"> <h1 style="margin: 0px; line-height: 140%; text-align: center; word-wrap: break-word; font-weight: normal; font-family: comic sans ms,sans-serif; font-size: 23px;"> 😀 We are Happy to see you 😀 </h1> </td> </tr> </tbody> </table> <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody> <tr> <td style="overflow-wrap:break-word;word-break:break-word;padding:2px;font-family:arial,helvetica,sans-serif;" align="left"> <table width="100%" cellpadding="0" cellspacing="0" border="0"> <tr> <td style="padding-right: 0px;padding-left: 0px;" align="center"> <img align="center" border="0" src="https://user-images.githubusercontent.com/38624002/167153626-c10c301a-fd95-4aee-b7ed-993d44f2004f.jpeg" alt="" title="" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: inline-block !important;border: none;height: auto;float: none;width: 100%;max-width: 266px;" width="266"/> </td> </tr> </table> </td> </tr> </tbody> </table> <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody> <tr> <td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left"> <table height="0px" align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;border-top: 1px solid #BBBBBB;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%"> <tbody> <tr style="vertical-align: top"> <td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top;font-size: 0px;line-height: 0px;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%"> <span> </span> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody> <tr> <td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left"> <div align="center"> <div style="display: table;"> <table align="left" border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 5px"> <tbody><tr style="vertical-align: top"><td align="left" valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top"> <a href="https://www.facebook.com/A7medQeshta/" title="Facebook" target="_blank"> <img src="https://user-images.githubusercontent.com/38624002/167153619-24dd5572-276b-4ea5-a9ba-ab004372bf56.png" alt="Facebook" title="Facebook" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important"> </a> </td></tr> </tbody></table> <table align="left" border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 5px"> <tbody><tr style="vertical-align: top"><td align="left" valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top"> <a href="https://twitter.com/ahmedqeshta0" title="Twitter" target="_blank"> <img src="https://user-images.githubusercontent.com/38624002/167153615-a17c1671-b9e0-4efa-8f25-d2b35e2729c4.png" alt="Twitter" title="Twitter" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important"> </a> </td></tr> </tbody></table> <table align="left" border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 5px"> <tbody><tr style="vertical-align: top"><td align="left" valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top"> <a href="https://github.com/AhmedQeshta" title="Twitter" target="_blank"> <img src="https://user-images.githubusercontent.com/38624002/167153630-cae46bcd-7db2-434f-a625-e345e6b3df58.png" alt="GitHub" title="GitHub" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important"> </a> </td></tr> </tbody></table> </div> <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody> <tr> <td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left"> <div style="line-height: 140%; text-align: center; word-wrap: break-word;"> <a href="${link}"> <button type="button" style="background: #34bbbc; color: #ffffff; padding: 1rem; font-size: 14px; line-height: 140%; border: none; cursor: pointer; border-radius: 8px;">Verify Your Email</button> </a> </div> </td> </tr> </tbody> </table> </div> </div> </div> </div> </div> </td> </tr> </tbody> </table> </body> </html> `;};
module.exports = { verifyEmail,};server\utils\jwt.js
const { sign, verify } = require('jsonwebtoken');
const { JWT_SECRET } = process.env;
const OPTIONS = { expiresIn: '30d', algorithm: 'HS256'};
module.exports = {
generateToken: (object, options = OPTIONS) => new Promise((resolve, reject) {
sign(object, JWT_SECRET, options, (error, payload) => {
if (error) return reject(error);
return resolve(payload);
});
}),
checkToken: (token) => new Promise((resolve, reject) => {
verify(token, JWT_SECRET, (error, payload) => {
if (error) return reject(error);
return resolve(payload);
});
})
};server\utils\CustomError.js
module.exports = { CustomError: (message, status, messages) => {
const error = new Error(message);
error.status = status; error.massages = massages; return error;
}};server\utils\validation\index.js
const addUserSchema = require('./addUserSchema');
const paramsValidation = require('./paramsValidation');
module.exports = { addUserSchema, paramsValidation,};server\utils\validation\paramsValidation.js
const Joi = require('joi');
const paramsValidation = Joi.object({
id: Joi.number().integer().positive().required(),
token: Joi.string().required()
});
module.exports = paramsValidation;server\utils\validation\addUserSchema.js
const Joi = require('joi');
const addUserSchema = Joi.object({
email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] }, }).required(),
password: Joi.string().pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{6,})/).min(6).required()
});
module.exports = addUserSchema;server\utils\index.js
const { CustomError } = require('./CustomError');
const { addUserSchema, paramsValidation } = require('./validation');
const { generateToken, checkToken } = require('./jwt');
module.exports = { CustomError, addUserSchema, generateToken, checkToken, paramsValidation};After creating a new user, we will send an email, Go to Mailtrap to test it,
In conclusion, as a user, when registering in any way, must by default set Column ‘verified’ to false, then after creating it in the database, the app will send an email with a verification link, this link containing the ID of this user and a token to check if he or not.
When clicked above, this link will change the status of the user from false to true, to be verified