비트베이크

Building a Secure SMS OTP System in NestJS with Redis in 5 Minutes

2026-04-10T01:01:38.139Z

NESTJS-REDIS-OTP

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:

  1. The user requests an OTP by providing their phone number.
  2. Our NestJS server generates a 6-digit random code and stores it in Redis with a 3-minute Time-To-Live (TTL).
  3. We trigger the EasyAuth API (POST /send) to instantly dispatch the SMS.
  4. 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

  1. Prevent SMS Bombing (Rate Limiting) Malicious bots might spam your /send endpoint, 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.

  2. 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!

Start advertising on Bitbake

Contact Us

More Articles

2026-06-04T01:04:15.823Z

The 2026 E-Commerce New Product Launch Survival Formula: Dominating Platform Search Rankings in 7 Days via Reward-Based Trials and Purchase Verification

2026-06-04T01:04:15.800Z

2026 이커머스 신제품 론칭 생존 공식: 리워드형 체험단과 구매 인증으로 7일 만에 플랫폼 검색 랭킹 장악하기

2026-06-01T01:01:58.264Z

Surviving the 2026 Cookieless Era for B2C: Building Zero-Party Data with Reward-Based Quiz Marketing

2026-06-01T01:01:58.231Z

2026 쿠키리스 시대의 B2C 생존법: 리워드 기반 퀴즈 마케팅으로 제로파티 데이터 구축하기

Services

HomeFeedFAQCustomer Service

Inquiry

Bitbake

LAEM Studio | Business Registration No.: 542-40-01042

4th Floor, 402-J270, 16 Su-ro 116beon-gil, Wabu-eup, Namyangju-si, Gyeonggi-do

TwitterInstagramNaver Blog