Chat Widget
Chat with us!

Text.lk SMS Gateway Sri Lanka | Cheapest price on the market (0.64LKR per SMS)

  +94 77 644 00 80

Login         Register

How to Implement OTP (One-Time Password) Verification Using the MERN Stack & Text.lk SMS (MongoDB, Express, React, Node.js)

Below is a complete OTP verification implementation guide for the MERN stack (MongoDB, Express, React, Node.js) using Text.lk SMS.


OTP (One-Time Password) verification is widely used in modern web and mobile applications for:

  • Passwordless login
  • Mobile number verification
  • Secure API access
  • Transaction confirmation

In this tutorial, you’ll learn how to build a secure OTP verification system using the MERN stack with Text.lk SMS Gateway.


OTP Verification Flow (MERN)

  1. User enters mobile number (React)
  2. Backend generates OTP (Node.js)
  3. OTP is stored in MongoDB (hashed)
  4. OTP is sent via Text.lk SMS
  5. User enters OTP
  6. OTP is verified and user is authenticated

Project Structure

mern-otp/
├── server/
   ├── config/
      └── db.js
   ├── models/
      └── Otp.js
   ├── routes/
      └── otp.js
   ├── controllers/
      └── otpController.js
   ├── utils/
      └── sms.js
   ├── server.js
   └── .env
└── client/
    ├── src/
       ├── components/
          └── OtpForm.jsx
       └── App.js

Step 1: Backend Dependencies

cd server
npm init -y
npm install express mongoose bcryptjs dotenv moment cors
npm install textlk-node

Step 2: Environment Configuration (server/.env)

PORT=5000
MONGO_URI=mongodb://localhost:27017/otp_db

TEXTLK_API_KEY=your_textlk_api_key
TEXTLK_SENDER_ID=TEXTLK
OTP_EXPIRY_MINUTES=5

Step 3: MongoDB Connection (config/db.js)

const mongoose = require('mongoose');

const connectDB = async () => {
    await mongoose.connect(process.env.MONGO_URI);
    console.log('MongoDB connected');
};

module.exports = connectDB;

Step 4: OTP Model (models/Otp.js)

const mongoose = require('mongoose');

const otpSchema = new mongoose.Schema({
    mobile: { type: String, required: true },
    otpHash: { type: String, required: true },
    expiresAt: { type: Date, required: true },
    isVerified: { type: Boolean, default: false }
}, { timestamps: true });

module.exports = mongoose.model('Otp', otpSchema);

Step 5: Text.lk SMS Utility (utils/sms.js)

const TextLK = require('textlk-node');

const sms = new TextLK({
    apiKey: process.env.TEXTLK_API_KEY
});

const sendSMS = async (mobile, message) => {
    return sms.send({
        recipient: mobile,
        sender_id: process.env.TEXTLK_SENDER_ID,
        type: 'plain',
        message
    });
};

module.exports = sendSMS;

Step 6: OTP Controller (controllers/otpController.js)

Send OTP

const Otp = require('../models/Otp');
const bcrypt = require('bcryptjs');
const moment = require('moment');
const sendSMS = require('../utils/sms');

exports.sendOtp = async (req, res) => {
    const { mobile } = req.body;

    if (!mobile) {
        return res.status(400).json({ message: 'Mobile number required' });
    }

    const otp = Math.floor(100000 + Math.random() * 900000);
    const otpHash = await bcrypt.hash(otp.toString(), 10);

    await Otp.create({
        mobile,
        otpHash,
        expiresAt: moment().add(process.env.OTP_EXPIRY_MINUTES, 'minutes')
    });

    await sendSMS(
        mobile,
        `Your OTP code is ${otp}. Valid for ${process.env.OTP_EXPIRY_MINUTES} minutes.`
    );

    res.json({ status: 'success', message: 'OTP sent successfully' });
};

Verify OTP

exports.verifyOtp = async (req, res) => {
    const { mobile, otp } = req.body;

    const record = await Otp.findOne({ mobile, isVerified: false }).sort({ createdAt: -1 });

    if (!record) {
        return res.status(404).json({ message: 'OTP not found' });
    }

    if (moment(record.expiresAt).isBefore(moment())) {
        return res.status(400).json({ message: 'OTP expired' });
    }

    const isValid = await bcrypt.compare(otp.toString(), record.otpHash);

    if (!isValid) {
        return res.status(400).json({ message: 'Invalid OTP' });
    }

    record.isVerified = true;
    await record.save();

    res.json({ status: 'success', message: 'OTP verified successfully' });
};

Step 7: Routes (routes/otp.js)

const express = require('express');
const router = express.Router();
const otpController = require('../controllers/otpController');

router.post('/send', otpController.sendOtp);
router.post('/verify', otpController.verifyOtp);

module.exports = router;

Step 8: Server Bootstrap (server.js)

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const connectDB = require('./config/db');

const app = express();
connectDB();

app.use(cors());
app.use(express.json());

app.use('/api/otp', require('./routes/otp'));

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

Step 9: React Frontend Example

OTP Form (OtpForm.jsx)

import { useState } from 'react';
import axios from 'axios';

export default function OtpForm() {
    const [mobile, setMobile] = useState('');
    const [otp, setOtp] = useState('');
    const [step, setStep] = useState(1);

    const sendOtp = async () => {
        await axios.post('/api/otp/send', { mobile });
        setStep(2);
    };

    const verifyOtp = async () => {
        await axios.post('/api/otp/verify', { mobile, otp });
        alert('OTP Verified');
    };

    return (
        <div>
            {step === 1 && (
                <>
                    <input placeholder="07XXXXXXXX" onChange={e => setMobile(e.target.value)} />
                    <button onClick={sendOtp}>Send OTP</button>
                </>
            )}

            {step === 2 && (
                <>
                    <input placeholder="Enter OTP" onChange={e => setOtp(e.target.value)} />
                    <button onClick={verifyOtp}>Verify OTP</button>
                </>
            )}
        </div>
    );
}

Extra Feature: Resend OTP with Cooldown

const lastOtp = await Otp.findOne({ mobile }).sort({ createdAt: -1 });

if (lastOtp && moment().diff(lastOtp.createdAt, 'seconds') < 60) {
    return res.status(429).json({ message: 'Please wait before requesting another OTP' });
}

Security Best Practices

✔ Hash OTP before storing
✔ Short expiry (3–5 minutes)
✔ Rate-limit OTP requests
✔ Use HTTPS
✔ Auto-delete expired OTPs


🎯 Use Cases

  • Passwordless authentication
  • SaaS dashboards
  • Mobile app login
  • Payment verification
  • API security

🏁 Conclusion

You now have a secure, scalable OTP verification system using the MERN stack and Text.lk SMS.

This setup is:

  • Modern
  • API-ready
  • Mobile-friendly
  • Enterprise-grade

Leave a Reply

Your email address will not be published. Required fields are marked *