Phone Verification with Node.js
Build a complete OTP phone verification flow using Node.js, Express, and CastBrick
Phone Verification with Node.js
This tutorial builds a production-ready OTP (one-time password) flow: a user enters their phone number, receives a 6-digit code via SMS, and verifies it to confirm their identity.
What you'll build
POST /auth/send-otp → generates a code, sends it via CastBrick
POST /auth/verify-otp → validates the code and returns a session tokenPrerequisites
- Node.js 18+
- A CastBrick API key from the dashboard
Setup
mkdir otp-demo && cd otp-demo
npm init -y
npm install castbrick-js express dotenvCreate a .env file:
CASTBRICK_API_KEY=your_api_key_hereThe code
Create server.mjs:
import 'dotenv/config';
import express from 'express';
import { CastBrick, CastBrickApiError } from 'castbrick-js';
const app = express();
app.use(express.json());
const cb = new CastBrick({ apiKey: process.env.CASTBRICK_API_KEY });
// In production, replace this with Redis or a database
const otpStore = new Map(); // phone → { code, expiresAt, attempts }
const OTP_TTL_MS = 10 * 60 * 1000; // 10 minutes
const MAX_ATTEMPTS = 3;
function generateOtp() {
return String(Math.floor(100000 + Math.random() * 900000));
}
// POST /auth/send-otp
app.post('/auth/send-otp', async (req, res) => {
const { phone } = req.body;
if (!phone) {
return res.status(422).json({ error: 'phone is required' });
}
const code = generateOtp();
otpStore.set(phone, {
code,
expiresAt: Date.now() + OTP_TTL_MS,
attempts: 0,
});
try {
await cb.sms.send({
recipients: [phone],
content: `Your CastBrick verification code is ${code}. It expires in 10 minutes. Do not share it.`,
senderId: 'Verify',
});
res.json({ ok: true, message: 'Code sent' });
} catch (err) {
otpStore.delete(phone);
if (err instanceof CastBrickApiError) {
return res.status(err.status).json({ error: err.message });
}
res.status(500).json({ error: 'Failed to send SMS' });
}
});
// POST /auth/verify-otp
app.post('/auth/verify-otp', (req, res) => {
const { phone, code } = req.body;
if (!phone || !code) {
return res.status(422).json({ error: 'phone and code are required' });
}
const record = otpStore.get(phone);
if (!record) {
return res.status(400).json({ error: 'No code was sent to this number' });
}
if (Date.now() > record.expiresAt) {
otpStore.delete(phone);
return res.status(400).json({ error: 'Code has expired' });
}
record.attempts += 1;
if (record.attempts > MAX_ATTEMPTS) {
otpStore.delete(phone);
return res.status(429).json({ error: 'Too many attempts. Request a new code.' });
}
if (record.code !== code) {
return res.status(400).json({ error: 'Invalid code' });
}
otpStore.delete(phone);
// Replace with your real session/JWT logic
res.json({ ok: true, token: 'session_token_here' });
});
app.listen(3000, () => console.log('Server running on http://localhost:3000'));Test it
Send a code:
curl -X POST http://localhost:3000/auth/send-otp \
-H "Content-Type: application/json" \
-d '{"phone": "+244923000000"}'Verify it:
curl -X POST http://localhost:3000/auth/verify-otp \
-H "Content-Type: application/json" \
-d '{"phone": "+244923000000", "code": "123456"}'Going to production
Replace the in-memory otpStore with Redis to support multiple servers and survive restarts:
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
// Store OTP with 10-minute TTL
await redis.setEx(`otp:${phone}`, 600, JSON.stringify({ code, attempts: 0 }));
// Read
const raw = await redis.get(`otp:${phone}`);
const record = raw ? JSON.parse(raw) : null;
// Delete after successful verification
await redis.del(`otp:${phone}`);Never log OTP codes. In production, use HTTPS and add IP-based rate limiting (e.g., express-rate-limit) on the /auth/send-otp endpoint.
Next steps
- JavaScript SDK reference — all methods and types
- Error handling — handle 401, 402, 422
- Webhooks — track delivery status for your OTP messages