Building a Secure SMS OTP System in NestJS with Redis in 5 Minutes
![]()
Stuck on Paperwork Trying to Integrate SMS Verification?
If you're building a side project or startup MVP, you've probably hit this wall: you need SMS phone verification, but legacy API providers require business registration documents, proof of use, and a pre-registered caller ID. This bureaucratic nightmare can waste days before you even write a single line of code.
In this article, we will build a secure and scalable SMS One-Time Password (OTP) system using NestJS, Redis, and EasyAuth—a developer-focused SMS API that requires zero paperwork and lets you start sending messages in exactly 5 minutes.
Solution Overview
Our authentication system will follow this straightforward flow:
- The user requests an OTP by providing their phone number.
- Our NestJS server generates a 6-digit random code and stores it in Redis with a 3-minute Time-To-Live (TTL).
- We trigger the EasyAuth API (
POST /send) to instantly dispatch the SMS. - When the user submits the code, we verify it against the value stored in Redis.
> 💡 No Redis? No Problem!
> If setting up a Redis instance is too much overhead for your current MVP, EasyAuth provides a built-in POST /verify endpoint. This allows you to achieve completely stateless OTP verification using just two endpoints (/send and /verify) without any database at all!
Step-by-Step Implementation
1. Install Dependencies
We need ioredis to communicate with Redis and axios to make HTTP requests to the EasyAuth API.
npm install ioredis axios
2. Complete Code for AuthService
Here is a production-ready AuthService that handles the entire OTP lifecycle.
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import Redis from 'ioredis';
import axios from 'axios';
@Injectable()
export class AuthService {
private redisClient: Redis;
private readonly EASYAUTH_API_URL = 'https://api.easyauth.co.kr';
private readonly API_KEY = 'your_easyauth_api_key_here'; // Store this in your .env!
constructor() {
// Connects to localhost:6379 by default.
this.redisClient = new Redis();
}
async sendOtp(phoneNumber: string) {
// 1. Generate a random 6-digit OTP
const otp = Math.floor(100000 + Math.random() * 900000).toString();
try {
// 2. Save to Redis with a 3-minute (180s) TTL
await this.redisClient.set(`otp:${phoneNumber}`, otp, 'EX', 180);
// 3. Dispatch SMS via EasyAuth
// Auto-configured caller ID means no pre-registration required!
await axios.post(`${this.EASYAUTH_API_URL}/send`, {
to: phoneNumber,
text: `[MyService] Your verification code is [${otp}].`
}, {
headers: {
'Authorization': `Bearer ${this.API_KEY}`,
'Content-Type': 'application/json'
}
});
return { message: 'OTP sent successfully.' };
} catch (error) {
throw new HttpException('Failed to send SMS.', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async verifyOtp(phoneNumber: string, code: string) {
// 1. Retrieve OTP from Redis
const storedOtp = await this.redisClient.get(`otp:${phoneNumber}`);
if (!storedOtp) {
throw new HttpException('OTP expired or does not exist.', HttpStatus.BAD_REQUEST);
}
// 2. Compare the provided code
if (storedOtp !== code) {
throw new HttpException('Invalid OTP code.', HttpStatus.BAD_REQUEST);
}
// 3. On success, delete the OTP immediately to prevent reuse
await this.redisClient.del(`otp:${phoneNumber}`);
return { message: 'Phone number verified successfully.' };
}
}
3. Controller Setup
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('send')
async send(@Body('phoneNumber') phoneNumber: string) {
return this.authService.sendOtp(phoneNumber);
}
@Post('verify')
async verify(
@Body('phoneNumber') phoneNumber: string,
@Body('code') code: string
) {
return this.authService.verifyOtp(phoneNumber, code);
}
}
Tips & Best Practices
-
Prevent SMS Bombing (Rate Limiting)
Malicious bots might spam your/sendendpoint, driving up your SMS costs. Since you already have Redis, it's highly recommended to implement rate limiting. Restrict OTP requests to a maximum of 1 request per minute per phone number or IP address. -
Immediate OTP Invalidation
Always delete the OTP from Redis (this.redisClient.del()) immediately after a successful verification. This mitigates replay attacks where an attacker attempts to reuse a valid code before its TTL expires.
Conclusion
By pairing NestJS with Redis's excellent TTL features, you can build a robust custom OTP system with full control over the verification flow.
More importantly, as a solo developer or startup, you shouldn't waste your precious time on bureaucracy. Skip the paperwork and the legacy carriers. With [EasyAuth], you get an ultra-simple API, auto-configured caller IDs, and competitive pricing at just 15~25 KRW per message (half the price of traditional services).
Sign up today and get 10 free SMS credits to start building your MVP in 5 minutes!