Building a Node.js Server with Email OTP Authentication

Last Updated : 20 Jan, 2026

In today’s digital landscape, secure user authentication is essential for protecting user data and application integrity. Email OTP (One-Time Password) verification enhances security by confirming user identity through time-bound codes sent to email.

  • Builds a Node.js backend authentication server using industry-standard libraries
  • Implements email-based OTP verification for an added security layer
  • Follows a modern, scalable, and secure authentication approach suitable for real-world app.

Prerequisites

Before getting started, make sure you have the following:

  • Basic understanding of Node.js and JavaScript
  • Familiarity with a code editor (e.g., Visual Studio Code)
  • Node.js and npm installed on your system
  • Basic knowledge of REST APIs and MongoDB

Project Setup

1. Create a new project directory:

mkdir node-auth-otp
cd node-auth-otp

2. Initialize npm:

npm init -y

This will generate a package.json file to manage your project dependencies.

Dependencies

We'll utilize several popular npm packages:

  • Express.js: Web framework for building Node.js applications
  • Mongoose: ODM (Object Data Modeling) library for interacting with MongoDB
  • Nodemailer: Email sending functionality
  • bcrypt: Secure password hashing
  • dotenv: Environment variable management

Install them using npm:

npm install express mongoose nodemailer bcrypt dotenv

Database Setup (MongoDB)

1. Set up a MongoDB instance (locally or on a cloud provider).

2. Create a database (e.g., auth-db) for storing user information.

Environment Variables

Create a .env file in your project root and store sensitive details like your MongoDB connection string and email credentials:

MONGODB_URI=your_mongodb_connection_string
EMAIL_HOST=your_smtp_host
EMAIL_PORT=your_smtp_port
EMAIL_USER=your_email_username
EMAIL_PASS=your_email_password

Note: Never commit your .env file to version control.

Server Implementation (server.js)

Create the server.js file, where we configure the Node.js server, connect to the database, and define authentication-related API endpoints.

1. Import Required Modules

These dependencies provide essential functionality such as server creation, database interaction, email delivery, and security.

Node
const express = require('express');
const mongoose = require('mongoose');
const nodemailer = require('nodemailer');
const bcrypt = require('bcrypt');
const dotenv = require('dotenv');

dotenv.config();
  • express is used to build RESTful APIs.
  • mongoose enables structured interaction with MongoDB.
  • dotenv loads environment variables securely.

2. User Schema (MongoDB)

The user schema defines how authentication-related data is stored in MongoDB.

Node
const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  otp: String,
  otpExpire: Date
});

const User = mongoose.model('User', userSchema);
  • Stores user credentials in a structured format.
  • Ensures email uniqueness for each user.
  • Includes OTP and expiration fields for verification.

3. Connect to MongoDB

This establishes a secure connection between the Node.js application and MongoDB.

Node
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.error(err));
  • Uses environment variables for secure configuration.
  • Enables modern MongoDB connection features.
  • Logs connection status for debugging.

4. Initialize Express App

This sets up the Express application and enables JSON request handling.

Node
const app = express();
app.use(express.json());
  • Initializes the Express server and parses incoming JSON request bodies.
  • Prepares the app for API route definitions.

5. Generate OTP

This function creates a random six-digit OTP for verification.

Node
const generateOTP = () => {
  return Math.floor(100000 + Math.random() * 900000);
};
  • Generates a numeric 6-digit OTP
  • Ensures randomness for security
  • Can be replaced with a dedicated OTP library if needed

6. Send OTP via Email

This function sends the generated OTP to the user’s email address.

Node
const sendOTP = async (email, otp) => {
  const transporter = nodemailer.createTransport({
    host: process.env.EMAIL_HOST,
    port: process.env.EMAIL_PORT,
    secure: false,
    auth: {
      user: process.env.EMAIL_USER,
      pass: process.env.EMAIL_PASS
    }
  });

  const mailOptions = {
    from: 'Your App <noreply@yourapp.com>',
    to: email,
    subject: 'OTP Verification',
    text: `Your OTP is ${otp}. It will expire in 10 minutes.`
  };

  await transporter.sendMail(mailOptions);
};
  • Uses Nodemailer with SMTP configuration
  • Sends a time-bound OTP for security
  • Supports easy customization of email content

7. User Registration with OTP

This endpoint registers a new user and initiates email-based OTP verification.

Node
app.post('/register', async (req, res) => {
  const { email, password } = req.body;

  try {
    const otp = generateOTP();
    const hashedPassword = await bcrypt.hash(password, 10);

    const newUser = new User({
      email,
      password: hashedPassword,
      otp,
      otpExpire: new Date(Date.now() + 10 * 60 * 1000)
    });

    await newUser.save();
    await sendOTP(email, otp);

    res.status(201).json({
      message: 'Registration successful. Please verify your email using OTP.'
    });
  } catch (err) {
    res.status(500).json({ message: 'Registration failed.' });
  }
});
  • Hashes passwords securely using bcrypt.
  • Stores OTP with an expiration time.
  • Sends OTP immediately after successful registration.

8. OTP Verification Endpoint (Placeholder)

This endpoint will validate the OTP entered by the user.

Node
app.post('/verify-otp', async (req, res) => {
  // Implement OTP validation logic here
  res.json({ message: 'OTP verification endpoint' });
});
  • Intended to verify submitted OTP values.
  • Should handle expiration and mismatch cases.
  • Clears OTP data after successful verification.

Expected Verification Flow:

  • Find the user by email.
  • Compare submitted OTP with stored OTP.
  • Validate OTP expiration and remove it on success.

9. Start the Server

This launches the backend server and listens for incoming requests.

Node
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});
  • Starts the application on the defined port.
  • Uses environment variables for flexibility.
  • Confirms server status via console logging.

Complete server.js File

Here is the complete server.js file including full implementation:

JavaScript
const express = require('express');
const mongoose = require('mongoose');
const nodemailer = require('nodemailer');
const bcrypt = require('bcrypt');
const dotenv = require('dotenv');

dotenv.config(); // Load environment variables

// User schema for MongoDB
const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  otp: String, // Field to store OTP
  otpExpire: Date // Field to store OTP expiration time
});

const User = mongoose.model('User', userSchema);

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error(err));

const app = express();
app.use(express.json()); // Parse incoming JSON data

// Function to generate random OTP (placeholder)
const generateOTP = () => {
  // Implement your desired OTP generation logic here
  // (e.g., using a library like otp-generator)
  return Math.floor(100000 + Math.random() * 900000); // Example 6-digit OTP
};

// Function to send OTP via email
const sendOTP = async (email, otp) => {
  const transporter = nodemailer.createTransport({
    host: process.env.EMAIL_HOST,
    port: process.env.EMAIL_PORT,
    secure: false, // Adjust based on your SMTP server configuration
    auth: {
      user: process.env.EMAIL_USER,
      pass: process.env.EMAIL_PASS
    }
  });

  const mailOptions = {
    from: 'Your App Name <noreply@yourapp.com>', // Replace with your sender info
    to: email,
    subject: 'Your OTP for Node Auth App',
    text: `Your OTP is ${otp}. It will expire in 10 minutes.` // Customize message
  };

  try {
    await transporter.sendMail(mailOptions);
    console.log(`OTP sent to ${email}`);
  } catch (err) {
    console.error('Error sending OTP:', err);
  }
};

// Register a new user (with email OTP verification)
app.post('/register', async (req, res) => {
  const { email, password } = req.body; // Extract email and password

  // Validation (optional)
  // You can add checks for email format and password strength

  try {
    // Generate OTP
    const otp = generateOTP();

    // Hash password
    const saltRounds = 10; // Adjust saltRounds as needed
    const hashedPassword = await bcrypt.hash(password, saltRounds);

    // Create a new user
    const newUser = new User({
      email,
      password: hashedPassword,
      otp,
      otpExpire: new Date(Date.now() + 10 * 60 * 1000) // Set OTP expiration (10 minutes)
    });

    await newUser.save(); // Save user to database

    // Send OTP email
    await sendOTP(email, otp);

    res.status(201).json({ message: 'Registration successful! Please check your email for OTP.' });
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Error during registration.' });
  }
});

// Placeholder for OTP verification route (implement logic here)
app.post('/verify-otp', async (req, res) => {
  // ... (logic to verify OTP and handle success/failure)
  res.json({ message: 'Placeholder for OTP verification' });
});

const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Server listening on port ${port}`));

Security & Implementation Considerations

These points highlight essential steps and best practices to make the authentication system reliable and production-ready.

  • Replace placeholder logic with a secure OTP generation and reliable email delivery implementation.
  • Implement the /verify-otp endpoint to validate OTPs and check expiration time.
  • Add robust error handling and proper input validation throughout the codebase.
  • Apply production-level security practices such as rate limiting, logging, and secure configurations.
Comment

Explore