code examples
code examples
NestJS SMS 2FA with Twilio: Complete OTP Authentication Tutorial
Build secure two-factor authentication in NestJS using Twilio SMS API. Learn phone verification, OTP generation, JWT authentication, Prisma PostgreSQL integration, and production-ready security practices.
NestJS SMS 2FA with Twilio: Complete OTP Authentication Tutorial
Build secure SMS-based Two-Factor Authentication (2FA) in NestJS with Twilio. This comprehensive tutorial covers OTP phone verification, JWT authentication, Prisma ORM with PostgreSQL, bcrypt password hashing, and production-ready security practices including rate limiting and environment variable protection.
Last Updated: January 2025
What You'll Build
Build a secure NestJS backend application featuring:
- User Sign-up: Register users with email, phone number, and password
- User Login: Authenticate with email/password and receive a JSON Web Token (JWT)
- Phone Number Verification: Verify phone numbers via SMS OTP before enabling 2FA
- 2FA Enable/Disable: Secure endpoints for users to opt-in or opt-out of 2FA with OTP confirmation
- 2FA-Protected Login: Require SMS OTP after password verification when 2FA is enabled
Problem Solved: Add a critical security layer beyond passwords, mitigating risks from compromised credentials by requiring access to the user's registered phone.
Technologies Used:
- NestJS: Progressive Node.js framework for building efficient, scalable server-side applications. Provides modular architecture, built-in features (pipes, guards, modules), and TypeScript support.
- Node.js: JavaScript runtime environment.
- TypeScript: JavaScript superset adding static types, improving code quality and maintainability.
- Twilio: Cloud communications platform for sending SMS OTPs. Provides reliable API and developer-friendly tools. (Twilio SDK supports Node.js 14, 16, 18, 20, and 22 LTS, verified January 2025)
- PostgreSQL: Powerful, open-source object-relational database system. Provides reliability and robustness.
- Prisma: Modern database toolkit for Node.js and TypeScript. Simplifies database access with type safety, excellent developer experience, and migration management.
- JWT (JSON Web Tokens): Stateless session management after successful authentication.
- bcrypt: Secure password hashing library.
- Joi: Data validation library.
System Architecture:
Your system comprises these interacting components:
- Client (Browser/App): Interacts with the NestJS API
- NestJS API: Handles incoming requests, business logic, validation, authentication, and coordinates with Prisma and Twilio. Contains Controllers, Services, Guards, and Pipes.
- Prisma ORM: Manages database connections, queries, and migrations based on the defined schema. Acts as the data access layer.
- PostgreSQL Database: Stores user data, OTPs, and application state
- Twilio API: External service for sending SMS messages containing OTP codes and receiving delivery status updates
Prerequisites:
- Node.js: Version 20.x or 22.x LTS recommended (as of January 2025). Node.js 18.x enters end-of-life in April 2025 and should not be used for new projects. npm or yarn installed.
- PostgreSQL: Server installed and running
- Twilio Account: With credentials (Account SID, Auth Token, Twilio Phone Number)
- Knowledge: Basic understanding of TypeScript, Node.js, REST APIs, and relational databases
- Tools: Code editor (like VS Code) and API testing tool (like Postman or curl)
Security Best Practices:
- Environment Variables: Never commit sensitive credentials (Twilio keys, JWT secrets, database passwords) to version control. Always use environment variables and add
.envto your.gitignorefile. - OTP Expiration: Implement short expiration times for OTPs (typically 5-10 minutes) to minimize the window of vulnerability.
- Rate Limiting: Implement rate limiting on OTP generation and verification endpoints to prevent abuse and brute-force attacks.
- HTTPS Only: Always use HTTPS in production to encrypt data in transit.
Final Outcome: A functional NestJS API with secure user authentication, phone verification, and optional SMS-based 2FA.
How Do You Set Up the NestJS Project?
Scaffold your NestJS project and configure the basic utilities.
Security Note: Before you begin, create a .gitignore file in your project root to protect sensitive information:
# Add to .gitignore
.env
.env.local
.env*.local
node_modules/
dist/1.1 Install NestJS CLI:
Install the NestJS CLI globally if you haven't already.
npm install -g @nestjs/cli1.2 Create New Project:
Generate a new NestJS project. Replace nestjs-twilio-2fa with your desired project name.
nest new nestjs-twilio-2fa
cd nestjs-twilio-2faThis creates a standard NestJS project structure.
1.3 Initial Cleanup (Optional):
The default project includes sample controller and service files. Remove them for a clean start.
- Delete
src/app.controller.ts,src/app.service.ts, andsrc/app.controller.spec.ts. - Open
src/app.module.tsand removeAppControllerfrom thecontrollersarray andAppServicefrom theprovidersarray.
src/app.module.ts (after cleanup):
import { Module } from '@nestjs/common';
@Module({
imports: [],
controllers: [], // Remove AppController
providers: [], // Remove AppService
})
export class AppModule {}1.4 Set Global API Prefix:
Prefix your API routes (e.g., /api/v1) for better organization.
Edit src/main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Set global prefix
app.setGlobalPrefix('api/v1');
await app.listen(process.env.PORT || 3000); // Use environment variable for port
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();1.5 Configure Logging Middleware:
NestJS has built-in logging, but a custom middleware provides more request context.
Create src/common/middleware/logger.middleware.ts:
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private logger = new Logger('HTTP'); // Context for logger
use(request: Request, response: Response, next: NextFunction): void {
const { ip, method, originalUrl } = request;
const userAgent = request.get('user-agent') || '';
response.on('finish', () => {
const { statusCode } = response;
const contentLength = response.get('content-length');
this.logger.log(
`${method} ${originalUrl} ${statusCode} ${contentLength || 0} - ${userAgent} ${ip}`,
);
});
next();
}
}Register the middleware globally in src/app.module.ts:
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware'; // Adjust path if needed
@Module({
imports: [],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*'); // Apply to all routes
}
}1.6 Custom Exception Filter:
Handle errors gracefully and provide consistent error responses.
Create the custom filter (src/common/filters/custom-exception.filter.ts):
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch() // Catch all types of exceptions
export class CustomExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(CustomExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status: number;
let message: string;
let errorName: string;
let stack: string | undefined;
if (exception instanceof HttpException) {
status = exception.getStatus();
const errorResponse = exception.getResponse();
message = typeof errorResponse === 'string'
? errorResponse
: (errorResponse as any)?.message || exception.message;
errorName = exception.constructor.name;
} else if (exception instanceof Error) {
// Handle generic errors
status = (exception as any)?.statusCode || HttpStatus.INTERNAL_SERVER_ERROR;
message = exception.message || 'Internal server error';
errorName = exception.name || 'InternalServerError';
stack = exception.stack;
} else {
// Handle non-Error exceptions
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = 'An unexpected error occurred';
errorName = 'UnknownError';
try {
stack = JSON.stringify(exception);
} catch {
stack = 'Could not stringify the unknown exception.';
}
}
// Log the error
this.logger.error(
`HTTP Status: ${status} Error: ${errorName} Message: ${message} Path: ${request.url}`,
stack,
CustomExceptionFilter.name,
);
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
error: errorName,
message: message,
});
}
}Register the filter globally in src/main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './common/filters/custom-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api/v1');
app.useGlobalFilters(new CustomExceptionFilter()); // Register filter
await app.listen(process.env.PORT || 3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();Why these choices?
- Global Prefix: Standardizes API access and enables versioning
- Logger Middleware: Provides essential request visibility for debugging and monitoring
- Custom Exception Filter: Ensures consistent, informative error responses for clients and detailed server-side logging, crucial for production environments
How Do You Configure Prisma with PostgreSQL?
Set up PostgreSQL and use Prisma to interact with it.
2.1 Set up PostgreSQL Database:
Create your database using psql or a GUI tool.
-- Connect using psql (replace 'postgres' with your superuser if different)
-- psql -U postgres
CREATE DATABASE nest_twilio_2fa_dev; -- Or your preferred name2.2 Install Prisma:
Install the Prisma CLI as a development dependency and the Prisma Client.
npm install -D prisma
npm install @prisma/client2.3 Initialize Prisma:
Create a prisma directory with a schema.prisma file and a .env file at the project root.
npx prisma init --datasource-provider postgresql2.4 Configure Database Connection:
Prisma automatically creates a .env file. Update the DATABASE_URL with your PostgreSQL connection string.
.env:
# Prisma Database Connection
DATABASE_URL="postgresql://YOUR_DB_USER:YOUR_DB_PASSWORD@YOUR_DB_HOST:YOUR_DB_PORT/YOUR_DB_NAME?schema=public"
# Example:
# DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/nest_twilio_2fa_dev?schema=public"- Replace placeholders (
YOUR_DB_USER,YOUR_DB_PASSWORD, etc.) with your actual database credentials. - Ensure
schema=publicis present if you're using the default schema. - Enclose the entire connection string in double quotes.
2.5 Define Data Models:
Open prisma/schema.prisma and define the User and Otp models.
prisma/schema.prisma:
// Prisma schema file
// Learn more: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
phone String @unique // Ensure phone numbers are unique
password String // Hashed password
firstName String
lastName String
twoFA Boolean @default(false) // 2FA status
isPhoneVerified Boolean @default(false) // Phone verification status
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
otps Otp[] // Relation to Otp model
}
model Otp {
id String @id @default(uuid())
code String // The OTP code itself (6 digits recommended)
userId String // Foreign key to User
useCase UseCase // Purpose of the OTP
expiresAt DateTime // Expiry timestamp (recommended: 5-10 minutes from creation)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, useCase]) // Index for faster lookups
@@index([expiresAt]) // Index for cleanup queries
}
// Enum for different OTP use cases
enum UseCase {
PHONE_VERIFICATION // For verifying phone number initially
TWO_FACTOR_AUTH // For logging in when 2FA is enabled
DISABLE_TWO_FACTOR // Optional: For confirming disabling 2FA
}Model Details:
- User: Stores user details, including flags for
twoFAandisPhoneVerified - Otp: Stores OTP codes linked to a user with a specific
useCaseandexpiresAttimestamp. Best Practice: OTPs should expire within 5-10 minutes to minimize security risks. - UseCase Enum: Defines distinct purposes for OTP generation, improving clarity and logic
- onDelete: Cascade: If a User is deleted, their associated OTPs are also deleted
- @@index: Improves query performance when finding OTPs for a specific user and use case. Added index on
expiresAtfor efficient cleanup of expired OTPs.
2.6 Run Database Migration:
Apply the schema changes to your database. Prisma generates SQL migration files and runs them.
npx prisma migrate dev --name init_user_otp_schemaThis command will:
- Create a
prisma/migrationsfolder - Generate a SQL migration file based on your schema changes
- Apply the migration to your database, creating the
UserandOtptables - Generate/update the Prisma Client based on your schema
Important: Whenever you change prisma/schema.prisma, run npx prisma migrate dev --name <descriptive_migration_name> to update your database and npx prisma generate to update the Prisma Client typings.
2.7 Create Prisma Service:
Encapsulate Prisma Client logic within a dedicated NestJS service for better organization and testability.
Generate the module and service:
nest generate module prisma
nest generate service prisma --no-spec # No spec file needed for this simple serviceImplement the service (src/prisma/prisma.service.ts):
import { Injectable, OnModuleInit, INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
// Connect to the database when the module is initialized
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
// Ensure Prisma disconnects gracefully on application shutdown
process.on('beforeExit', async () => {
console.log('Closing NestJS application and disconnecting Prisma Client...');
await app.close();
console.log('Prisma Client disconnected.');
});
}
}Configure the module (src/prisma/prisma.module.ts):
import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global() // Make PrismaService available globally
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}Import PrismaModule into the root AppModule (src/app.module.ts):
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { PrismaModule } from './prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}Enable shutdown hooks in src/main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CustomExceptionFilter } from './common/filters/custom-exception.filter';
import { PrismaService } from './prisma/prisma.service';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api/v1');
app.useGlobalFilters(new CustomExceptionFilter());
// Get PrismaService instance and enable shutdown hooks
const prismaService = app.get(PrismaService);
await prismaService.enableShutdownHooks(app);
await app.listen(process.env.PORT || 3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();Why these choices?
- Prisma: Provides type safety, auto-completion, and simplifies database interactions and migrations significantly
- Prisma Service: Decouples database logic from business logic, adhering to SOLID principles
- Global Module: Makes the
PrismaServiceeasily injectable across the application - Shutdown Hooks: Ensures the database connection closes properly when the application stops
- UUIDs: Better for distributed systems and avoids exposing sequential information compared to auto-increment integers
How Do You Implement User Registration?
Create the endpoints and logic for user registration.
3.1 Install Dependencies:
npm install joi bcrypt class-validator class-transformer
npm install -D @types/bcrypt @types/joi- joi: Request validation schemas used with our custom pipe
- bcrypt: Secure password hashing
- class-validator, class-transformer: Standard NestJS validation libraries
3.2 Create Account Module, Controller, Service:
nest generate module account
nest generate controller account --no-spec
nest generate service account --no-spec3.3 Create Validation Pipe:
Create a pipe to validate incoming request bodies using Joi schemas.
Create src/common/pipes/joi-validation.pipe.ts:
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
import { ObjectSchema } from 'joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
// Only validate the body of the request
if (metadata.type !== 'body' || !value) {
return value;
}
const { error, value: validatedValue } = this.schema.validate(value, {
abortEarly: false, // Return all validation errors
allowUnknown: false, // Disallow properties not defined in the schema
stripUnknown: true, // Remove unknown properties instead of throwing error
});
if (error) {
// Format error messages for better readability
const errorMessages = error.details.map((d) => d.message.replace(/"/g, "'")).join('; ');
throw new BadRequestException(`Validation failed: ${errorMessages}`);
}
return validatedValue;
}
}3.4 Create Sign-up DTO and Schema:
Define the expected request body structure.
Create src/account/dto/create-user.dto.ts:
// DTO helps with type inference in service/controller
export class CreateUserDto {
email: string;
phone: string;
password: string;
firstName: string;
lastName: string;
}Create the Joi validation schema src/account/validation/signup.schema.ts:
import * as Joi from 'joi';
// Basic password complexity: at least 8 chars, 1 letter, 1 number
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
// Basic phone number regex (adjust as needed for E.164 format)
const phoneRegex = /^[+\d\s-]+$/;
export const signupSchema = Joi.object({
email: Joi.string().email().required().messages({
'string.email': 'Email must be a valid email address',
'any.required': 'Email is required',
}),
phone: Joi.string().pattern(phoneRegex).required().messages({
'string.pattern.base': 'Phone number format is invalid',
'any.required': 'Phone number is required',
}),
password: Joi.string().pattern(passwordRegex).required().messages({
'string.pattern.base': 'Password must be at least 8 characters long and include at least one letter and one number',
'any.required': 'Password is required',
}),
firstName: Joi.string().trim().min(1).required().messages({
'string.empty': 'First name cannot be empty',
'any.required': 'First name is required',
}),
lastName: Joi.string().trim().min(1).required().messages({
'string.empty': 'Last name cannot be empty',
'any.required': 'Last name is required',
}),
});3.5 Create Password Hashing Utility:
Create src/common/utils/password.util.ts:
import * as bcrypt from 'bcrypt';
// OWASP recommends 10-12 rounds as of 2025
// Source: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
const SALT_ROUNDS = 10;
export const hashPassword = async (password: string): Promise<string> => {
return bcrypt.hash(password, SALT_ROUNDS);
};
export const verifyPassword = async (
password: string,
hash: string,
): Promise<boolean> => {
return bcrypt.compare(password, hash);
};3.6 Implement Sign-up Service Logic:
Inject PrismaService and implement the sign-up logic.
Edit src/account/account.service.ts:
import { Injectable, ConflictException, InternalServerErrorException, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { hashPassword } from '../common/utils/password.util';
import { User } from '@prisma/client';
import { Prisma } from '@prisma/client';
@Injectable()
export class AccountService {
private readonly logger = new Logger(AccountService.name);
constructor(private prisma: PrismaService) {}
async signUp(createUserDto: CreateUserDto): Promise<Omit<User, 'password'>> {
const { email, phone, password, firstName, lastName } = createUserDto;
// 1. Check if user with email or phone already exists
const existingUser = await this.prisma.user.findFirst({
where: { OR: [{ email }, { phone }] },
select: { email: true, phone: true }
});
if (existingUser) {
if (existingUser.email === email) {
this.logger.warn(`Signup attempt failed: Email ${email} already exists.`);
throw new ConflictException('User with this email already exists');
}
if (existingUser.phone === phone) {
this.logger.warn(`Signup attempt failed: Phone ${phone} already exists.`);
throw new ConflictException('User with this phone number already exists');
}
}
// 2. Hash the password
const hashedPassword = await hashPassword(password);
// 3. Create the user
try {
const user = await this.prisma.user.create({
data: {
email,
phone,
password: hashedPassword,
firstName,
lastName,
},
});
this.logger.log(`User created successfully: ${user.id} / ${user.email}`);
// 4. Omit password from the returned user object
const { password: _, ...result } = user;
return result;
} catch (error) {
// Catch unique constraint violations (race condition safeguard)
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
const target = (error.meta as any)?.target;
this.logger.warn(`Signup failed due to unique constraint violation on: ${target}`);
if (target && target.includes('email')) {
throw new ConflictException('User with this email already exists.');
} else if (target && target.includes('phone')) {
throw new ConflictException('User with this phone number already exists.');
} else {
throw new ConflictException('A user with these details already exists.');
}
}
// Log and re-throw other errors
this.logger.error(`Error during user signup: ${error.message}`, error.stack);
throw new InternalServerErrorException('Could not create user account.');
}
}
// Additional account service methods will go here
}3.7 Implement Sign-up Controller Endpoint:
Define the route handler in the controller.
Edit src/account/account.controller.ts:
import { Controller, Post, Body, UsePipes, HttpStatus, HttpCode } from '@nestjs/common';
import { AccountService } from './account.service';
import { CreateUserDto } from './dto/create-user.dto';
import { signupSchema } from './validation/signup.schema';
import { JoiValidationPipe } from '../common/pipes/joi-validation.pipe';
import { User } from '@prisma/client';
@Controller('account') // Route prefix: /api/v1/account
export class AccountController {
constructor(private readonly accountService: AccountService) {}
@Post('signup') // POST /api/v1/account/signup
@UsePipes(new JoiValidationPipe(signupSchema)) // Apply Joi validation
@HttpCode(HttpStatus.CREATED) // Set default success status to 201
async create(
@Body() createUserDto: CreateUserDto,
): Promise<Omit<User, 'password'>> {
return this.accountService.signUp(createUserDto);
}
// Additional account controller methods
}Ensure the AccountService and AccountController are correctly listed in src/account/account.module.ts.
src/account/account.module.ts:
import { Module } from '@nestjs/common';
import { AccountService } from './account.service';
import { AccountController } from './account.controller';
@Module({
controllers: [AccountController],
providers: [AccountService],
exports: [AccountService] // Export if needed by other modules like AuthModule
})
export class AccountModule {}Import AccountModule into the root AppModule (src/app.module.ts):
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { PrismaModule } from './prisma/prisma.module';
import { AccountModule } from './account/account.module';
@Module({
imports: [
PrismaModule,
AccountModule
],
controllers: [],
providers: [],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}Why these choices?
- Joi Pipe: Provides robust, schema-based validation at the entry point of the controller
- DTO: Enforces type safety within TypeScript code
- bcrypt: Industry standard for password hashing
- Conflict Check: Prevents duplicate user registrations based on unique fields with robust Prisma error handling
- Password Omission: Never return the hashed password to the client
How Do You Implement JWT Authentication?
Set up the initial login mechanism using email and password, returning a JWT if credentials are valid but before handling the 2FA OTP step.
4.1 Install JWT Dependency:
npm install @nestjs/jwt dotenv4.2 Configure JWT Module:
Define a JWT secret in your .env file. Use a strong, randomly generated secret in production.
.env:
DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/nest_twilio_2fa_dev?schema=public"
# Generate a strong secret (e.g., using openssl rand -hex 32)
JWT_SECRET="your-very-strong-and-secret-jwt-key-CHANGE-ME"
JWT_EXPIRATION_TIME="2h" # Example: Token valid for 2 hours
# Twilio Configuration (add these when implementing Twilio integration)
TWILIO_ACCOUNT_SID="your_twilio_account_sid"
TWILIO_AUTH_TOKEN="your_twilio_auth_token"
TWILIO_PHONE_NUMBER="your_twilio_phone_number"
# OTP Configuration
OTP_EXPIRATION_MINUTES="10" # OTP validity period (recommended: 5-10 minutes)
OTP_LENGTH="6" # Standard 6-digit OTPCreate a configuration file for easy access to constants (src/common/config/config.ts):
import * as dotenv from 'dotenv';
dotenv.config(); // Load .env file variables
export const config = {
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRATION_TIME || '1h',
},
twilio: {
accountSid: process.env.TWILIO_ACCOUNT_SID,
authToken: process.env.TWILIO_AUTH_TOKEN,
phoneNumber: process.env.TWILIO_PHONE_NUMBER,
},
otp: {
expirationMinutes: parseInt(process.env.OTP_EXPIRATION_MINUTES || '10', 10),
length: parseInt(process.env.OTP_LENGTH || '6', 10),
},
};
// Validate essential config on startup
if (!config.jwt.secret || config.jwt.secret.length < 32) {
console.error("FATAL ERROR: JWT_SECRET environment variable is missing or too short (minimum 32 characters recommended).");
process.exit(1);
}
if (!config.twilio.accountSid || !config.twilio.authToken || !config.twilio.phoneNumber) {
console.warn("WARNING: Twilio credentials not configured. SMS functionality will not work.");
}Related Resources
NestJS Security:
- Authentication and Authorization in NestJS
- Rate Limiting and Throttling with NestJS
- Environment Variable Management Best Practices
Twilio Integration:
- Twilio SMS API Documentation
- Phone Number Verification Patterns
- SMS Delivery Tracking and Error Handling
Database and ORM:
- Prisma Schema Design Best Practices
- PostgreSQL Performance Optimization
- Database Migration Strategies with Prisma