Skip to main content

Overview

The Resend OTP module adds secure email-based one-time password (OTP) verification to your Node.js Express application using the Resend API. The module automatically:
  • Installs all required dependencies
  • Generates controllers, routes, and middleware
  • Patches your main app file intelligently
  • Injects environment variables to .env
  • Supports both JavaScript and TypeScript
  • Includes rate limiting for security
OTPs are stored in-memory with a 5-minute expiration. For production, consider implementing a persistent storage solution like Redis.

Installation

Run the following command in your project directory:
npx devark add resend-otp

Interactive Setup

The CLI will guide you through an interactive setup process with the following prompts:
1

Select Language

Choose between JavaScript or TypeScript implementation
? Which version do you want to add for this module?
› JavaScript
  TypeScript
2

Specify Entry File

Enter your project’s main entry file (relative to root)
? Enter your project entry file (relative to root):
› app.js (for JavaScript)
› src/app.ts (for TypeScript)
For TypeScript projects, if the file doesn’t exist, Devark will auto-detect .ts files in your src/ directory.
3

Resend API Key

Enter your Resend API key (or leave empty for sample values)
? Enter your Resend API Key (leave empty for sample):
› sample-resend-api-key
Get your API key from Resend Dashboard
4

From Email Address

Enter the email address to send OTPs from
? Enter your FROM email address (leave empty for sample):
[email protected]
For production, use a verified domain in Resend. The default [email protected] is for testing only.

Dependencies Installed

The module automatically installs the following packages:

Runtime Dependencies

resend
express
express-rate-limit
dotenv

TypeScript Dev Dependencies (TypeScript only)

typescript
ts-node
@types/node
@types/express

Generated File Structure

your-project/
├── controllers/
│   ├── otp.js              # OTP generation and verification logic
│   └── otpFunctions.js     # Request handlers
├── routes/
│   └── otpRoutes.js        # API endpoints
├── middleware/
│   └── rateLimit.js        # Rate limiting configuration
├── app.js                   # Patched with OTP routes
└── .env                     # Environment variables

Configuration

The following environment variables are automatically added to your .env file:
RESEND_API_KEY
string
required
Your Resend API key from the Resend Dashboard
FROM_EMAIL
string
required
The email address to send OTPs from. Must be a verified domain in Resend for production use.

Example .env

RESEND_API_KEY=re_123456789
FROM_EMAIL=[email protected]

Generated Code

OTP Logic

The module generates controllers/otp.js:
import { Resend } from "resend";
import dotenv from "dotenv";
import crypto from "crypto";

dotenv.config();

const resend = new Resend(process.env.RESEND_API_KEY);
const otpStore = new Map();
const OTP_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes

export async function sendOtp(email) {
  const otp = crypto.randomInt(100000, 1000000).toString();
  const expiresAt = Date.now() + OTP_EXPIRY_MS;

  otpStore.set(email, { otp, expiresAt });

  const htmlContent = `
    <p>Hello,</p>
    <p>Your OTP for logging in is:</p>
    <p><strong>${otp}</strong></p>
    <p>This OTP will expire in 5 minutes. Do not share it with anyone.</p>
  `;

  try {
    await resend.emails.send({
      from: process.env.FROM_EMAIL,
      to: email,
      subject: "Your OTP is",
      html: htmlContent,
    });
    return true;
  } catch (err) {
    console.error("Failed to send OTP:", err);
    return false;
  }
}

export function verifyOtp(email, otp) {
  const entry = otpStore.get(email);
  if (!entry) return false;

  if (Date.now() > entry.expiresAt) {
    otpStore.delete(email);
    return false;
  }

  return entry.otp === otp;
}

Request Handlers

The module generates controllers/otpFunctions.js:
import { sendOtp, verifyOtp } from "./otp.js";

export async function sendOtpHandler(req, res) {
  const { email } = req.body;

  if (!email) {
    return res.status(400).json({ error: "Email required" });
  }

  const sent = await sendOtp(email);

  if (sent) {
    return res.json({
      message: "OTP sent successfully",
      success: true,
    });
  } else {
    return res.status(500).json({
      message: "Failed to send OTP",
      success: false,
    });
  }
}

export function verifyOtpHandler(req, res) {
  const { email, otp } = req.body;

  if (!email || !otp) {
    return res.status(400).json({ error: "Email and OTP are required." });
  }

  const valid = verifyOtp(email, otp);

  if (valid) {
    return res.status(200).json({ message: "correct" });
  } else {
    return res.status(401).json({ error: "incorrect" });
  }
}

API Routes

The module generates routes/otpRoutes.js:
import express from "express";
import {
  sendOtpHandler,
  verifyOtpHandler,
} from "../controllers/otpFunctions.js";
import { rateLimiter } from "../middleware/rateLimit.js";

const router = express.Router();

router.post("/send-otp", rateLimiter, sendOtpHandler);
router.post("/verify-otp", rateLimiter, verifyOtpHandler);

export default router;

Rate Limiting Middleware

The module generates middleware/rateLimit.js:
import rateLimit from "express-rate-limit";

export const rateLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10, // Limit each IP to 10 requests per windowMs
  message: {
    success: false,
    errors: "too many requests, please try again later.",
    status: 429,
  },
  standardHeaders: true,
  legacyHeaders: false,
});

API Endpoints

Send OTP

Sends a 6-digit OTP to the specified email address.
POST /send-otp
Content-Type: application/json

{
  "email": "[email protected]"
}
Success Response (200):
{
  "message": "OTP sent successfully",
  "success": true
}
Error Response (400):
{
  "error": "Email required"
}
Error Response (500):
{
  "message": "Failed to send OTP",
  "success": false
}

Verify OTP

Verifies the OTP provided by the user.
POST /verify-otp
Content-Type: application/json

{
  "email": "[email protected]",
  "otp": "123456"
}
Success Response (200):
{
  "message": "correct"
}
Error Response (400):
{
  "error": "Email and OTP are required."
}
Error Response (401):
{
  "error": "incorrect"
}

Usage Example

Frontend Integration

// Send OTP
async function sendOTP(email) {
  const response = await fetch('http://localhost:3000/send-otp', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email })
  });
  return response.json();
}

// Verify OTP
async function verifyOTP(email, otp) {
  const response = await fetch('http://localhost:3000/verify-otp', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, otp })
  });
  return response.json();
}

Complete Flow Example

1

User Requests OTP

const result = await sendOTP('[email protected]');
if (result.success) {
  console.log('OTP sent! Check your email.');
}
2

User Receives Email

The user receives an email with a 6-digit OTP that expires in 5 minutes.
3

User Submits OTP

const result = await verifyOTP('[email protected]', '123456');
if (result.message === 'correct') {
  console.log('Verified! Proceed with login.');
} else {
  console.log('Invalid or expired OTP.');
}

Security Features

Rate Limiting

Limits each IP to 10 requests per 15 minutes to prevent abuse

OTP Expiration

OTPs automatically expire after 5 minutes

Secure Generation

Uses Node.js crypto module for cryptographically secure random numbers

Automatic Cleanup

Expired OTPs are automatically removed from storage

Customization

Change OTP Expiration Time

Edit controllers/otp.js (or otp.ts):
const OTP_EXPIRY_MS = 10 * 60 * 1000; // Change to 10 minutes

Customize Email Template

Edit the htmlContent in the sendOtp function:
const htmlContent = `
  <div style="font-family: Arial, sans-serif;">
    <h2>Your Verification Code</h2>
    <p>Enter this code to continue:</p>
    <h1 style="color: #4F46E5;">${otp}</h1>
    <p>Valid for 5 minutes</p>
  </div>
`;

Adjust Rate Limiting

Edit middleware/rateLimit.js:
export const rateLimiter = rateLimit({
  windowMs: 10 * 60 * 1000, // 10 minutes
  max: 5, // Limit to 5 requests
  // ...
});

Change OTP Length

Edit controllers/otp.js:
// For 4-digit OTP
const otp = crypto.randomInt(1000, 10000).toString();

// For 8-digit OTP
const otp = crypto.randomInt(10000000, 100000000).toString();

Troubleshooting

Solution:
  1. Verify your Resend API key is correct in .env
  2. Check that your FROM_EMAIL domain is verified in Resend
  3. For testing, use [email protected]
  4. Check Resend dashboard for delivery logs
# Test your API key
curl -X POST 'https://api.resend.com/emails' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json'
Solution: The rate limiter restricts each IP to 10 requests per 15 minutes. Either:
  • Wait for the rate limit window to reset
  • Adjust the limits in middleware/rateLimit.js
  • Use different IPs for testing
Solution:
  1. Ensure you’re using the exact email address for both send and verify
  2. Check that the OTP hasn’t expired (5-minute default)
  3. Verify the OTP code matches exactly (6 digits)
  4. Check server logs for any errors during verification
Solution: The module installs TypeScript types automatically. If you still see errors:
npm install --save-dev @types/express @types/node
Solution: The default in-memory storage isn’t suitable for production with multiple server instances. Consider:
  • Implementing Redis for distributed OTP storage
  • Using a database with TTL indexes (MongoDB, PostgreSQL)
  • Implementing a hash-based approach without server storage
Example Redis integration:
import Redis from 'ioredis';
const redis = new Redis();

// Store OTP
await redis.setex(`otp:${email}`, 300, otp); // 5 min expiry

// Verify OTP
const stored = await redis.get(`otp:${email}`);
const valid = stored === otp;

Getting a Resend API Key

1

Sign Up for Resend

Visit resend.com and create a free account.
2

Navigate to API Keys

Go to the API Keys page in your dashboard.
3

Create New API Key

Click Create API Key and give it a descriptive name.
4

Copy the Key

Copy the generated API key immediately - it won’t be shown again.
Store your API key securely and never commit it to version control!
5

Verify Your Domain (Production)

For production use, verify your sending domain:
  1. Go to Domains in Resend dashboard
  2. Add your domain
  3. Add the provided DNS records to your domain
  4. Wait for verification

Production Considerations

Before deploying to production, address these security and scalability concerns:
Replace the in-memory Map with Redis or a database to support multiple server instances.
Add logic to lock accounts after multiple failed OTP attempts.
Track OTP generation and verification for security auditing.
Replace [email protected] with your verified domain email.
Ensure all API requests are made over HTTPS to prevent interception.

Next Steps

Google OAuth

Add Google authentication alongside OTP

GitHub OAuth

Add GitHub authentication to your project

Build docs developers (and LLMs) love