In our web applications, we often need to send messages to our users. Doing that through email is enough in a lot of cases, but we can also use SMS. In this article, we look into how we can use Twilio for verifying phone numbers provided by our users and sending messages.
Setting up Twilio
First, we need to create a Twilio account. It is a straightforward process that doesn’t require us to provide a credit card number.
After creating the Twilio account, we need to set up a service. In Twilio, a service acts as a set of common configurations used to perform phone number verification.
To define a service, we need to go to the services dashboard. When choosing a name for the service, remember that our users can see it.
The crucial part of the above process is the service id. We need to use it along with the account sid and auth token that we can find in the console.
1import { Module } from '@nestjs/common';
2import { ConfigModule } from '@nestjs/config';
3import * as Joi from '@hapi/joi';
4
5@Module({
6 imports: [
7 ConfigModule.forRoot({
8 validationSchema: Joi.object({
9 TWILIO_ACCOUNT_SID: Joi.string().required(),
10 TWILIO_AUTH_TOKEN: Joi.string().required(),
11 TWILIO_VERIFICATION_SERVICE_SID: Joi.string().required()
12 // ...
13 })
14 }),
15 ],
16 // ...
17})
18export class AppModule {}1TWILIO_ACCOUNT_SID=...
2TWILIO_AUTH_TOKEN=...
3TWILIO_VERIFICATION_SERVICE_SID=...
4# ...Using Twilio with Node.js
To use Twilio with Node.js, we can use the official Twilio library. It comes with all necessary TypeScript declarations built-in.
1npm install twilioMake sure to install the correct library. A few months ago a malicious package called twilio-npm was published that aimed to compromise the machines of people who downloaded it. If you want to know more, check out this article.
Let’s create the SmsService that uses the above library along with our environment variables.
1import { Injectable } from '@nestjs/common';
2import { ConfigService } from '@nestjs/config';
3import { Twilio } from 'twilio';
4import { UsersService } from '../users/users.service';
5
6@Injectable()
7export default class SmsService {
8 private twilioClient: Twilio;
9
10 constructor(
11 private readonly configService: ConfigService
12 ) {
13 const accountSid = configService.get('TWILIO_ACCOUNT_SID');
14 const authToken = configService.get('TWILIO_AUTH_TOKEN');
15
16 this.twilioClient = new Twilio(accountSid, authToken);
17 }
18}Verifying phone numbers
For us, the first step in verifying phone numbers is adding additional fields in the User entity.
1import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2
3@Entity()
4class User {
5 @PrimaryGeneratedColumn()
6 public id: number;
7
8 @Column({ unique: true })
9 public email: string;
10
11 @Column()
12 public phoneNumber: string;
13
14 @Column({ default: false })
15 public isPhoneNumberConfirmed: boolean;
16
17 // ...
18}
19
20export default User;It is crucial for the phoneNumber to be in the right format. The Twilio documentation suggests a regular expression that we can use.
1import { IsEmail, IsString, IsNotEmpty, MinLength, Matches } from 'class-validator';
2
3export class RegisterDto {
4 @IsEmail()
5 email: string;
6
7 @IsString()
8 @IsNotEmpty()
9 name: string;
10
11 @IsString()
12 @IsNotEmpty()
13 @MinLength(7)
14 password: string;
15
16 @IsString()
17 @IsNotEmpty()
18 @Matches(/^\+[1-9]\d{1,14}$/)
19 phoneNumber: string;
20}
21
22export default RegisterDto;If we would like to be more strict with the phone number validation, we could use the Lookup API that Twilio provides. With it, we can make a request to the Twilio API every time our users set a phone number and check if it is valid. Remember that every request we make to the Lookup API costs a little.
Initiating the SMS verification
Let’s add a function to our SmsService that can initiate SMS verification.
1import { Injectable } from '@nestjs/common';
2import { ConfigService } from '@nestjs/config';
3import { Twilio } from 'twilio';
4import { UsersService } from '../users/users.service';
5
6@Injectable()
7export default class SmsService {
8 private twilioClient: Twilio;
9
10 constructor(
11 private readonly configService: ConfigService,
12 private readonly usersService: UsersService
13 ) {
14 const accountSid = configService.get('TWILIO_ACCOUNT_SID');
15 const authToken = configService.get('TWILIO_AUTH_TOKEN');
16
17 this.twilioClient = new Twilio(accountSid, authToken);
18 }
19
20 initiatePhoneNumberVerification(phoneNumber: string) {
21 const serviceSid = this.configService.get('TWILIO_VERIFICATION_SERVICE_SID');
22
23 return this.twilioClient.verify.services(serviceSid)
24 .verifications
25 .create({ to: phoneNumber, channel: 'sms' })
26 }
27}Let’s also create a SmsController that uses it.
1import {
2 Controller,
3 UseGuards,
4 UseInterceptors,
5 ClassSerializerInterceptor, Post, Req, BadRequestException,
6} from '@nestjs/common';
7import SmsService from './sms.service';
8import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
9import RequestWithUser from '../authentication/requestWithUser.interface';
10
11@Controller('sms')
12@UseInterceptors(ClassSerializerInterceptor)
13export default class SmsController {
14 constructor(
15 private readonly smsService: SmsService
16 ) {}
17
18 @Post('initiate-verification')
19 @UseGuards(JwtAuthenticationGuard)
20 async initiatePhoneNumberVerification(@Req() request: RequestWithUser) {
21 if (request.user.isPhoneNumberConfirmed) {
22 throw new BadRequestException('Phone number already confirmed');
23 }
24 await this.smsService.initiatePhoneNumberVerification(request.user.phoneNumber);
25 }
26}Requesting the above endpoint results in Twilio sending the SMS to the user.
Twilio figures out the language based on the country code in the phone number. We could override it by using the locale property:
1initiatePhoneNumberVerification(phoneNumber: string) {
2 const serviceSid = this.configService.get('TWILIO_VERIFICATION_SERVICE_SID');
3
4 return this.twilioClient.verify.services(serviceSid)
5 .verifications
6 .create({ to: phoneNumber, channel: 'sms', locale: 'en' })
7}Confirming the verification code
Now, we need to create a way for the users to send the verification code back to our API. To do that, let’s create an additional method in our SmsService:
1import { BadRequestException, Injectable } from '@nestjs/common';
2import { ConfigService } from '@nestjs/config';
3import { Twilio } from 'twilio';
4import { UsersService } from '../users/users.service';
5
6@Injectable()
7export default class SmsService {
8 private twilioClient: Twilio;
9
10 constructor(
11 private readonly configService: ConfigService,
12 private readonly usersService: UsersService
13 ) {
14 const accountSid = configService.get('TWILIO_ACCOUNT_SID');
15 const authToken = configService.get('TWILIO_AUTH_TOKEN');
16
17 this.twilioClient = new Twilio(accountSid, authToken);
18 }
19
20 async confirmPhoneNumber(userId: number, phoneNumber: string, verificationCode: string) {
21 const serviceSid = this.configService.get('TWILIO_VERIFICATION_SERVICE_SID');
22
23 const result = await this.twilioClient.verify.services(serviceSid)
24 .verificationChecks
25 .create({to: phoneNumber, code: verificationCode})
26
27 if (!result.valid || result.status !== 'approved') {
28 throw new BadRequestException('Wrong code provided');
29 }
30
31 await this.usersService.markPhoneNumberAsConfirmed(userId)
32 }
33
34 // ...
35}You might notice that we use the usersService.markPhoneNumberAsConfirmed method above. We first need to define it.
1import { Injectable } from '@nestjs/common';
2import { InjectRepository } from '@nestjs/typeorm';
3import { Repository } from 'typeorm';
4import User from './user.entity';
5
6@Injectable()
7export class UsersService {
8 constructor(
9 @InjectRepository(User)
10 private usersRepository: Repository<User>,
11 ) {}
12
13 markPhoneNumberAsConfirmed(userId: number) {
14 return this.usersRepository.update({ id: userId }, {
15 isPhoneNumberConfirmed: true
16 });
17 }
18
19 // ...
20}The last step of implementing the code verification is adding it to the controller.
1import {
2 Body,
3 Controller,
4 UseGuards,
5 UseInterceptors,
6 ClassSerializerInterceptor, Post, Req, BadRequestException,
7} from '@nestjs/common';
8import SmsService from './sms.service';
9import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
10import RequestWithUser from '../authentication/requestWithUser.interface';
11import CheckVerificationCodeDto from './checkVerificationCode.dto';
12
13@Controller('sms')
14@UseInterceptors(ClassSerializerInterceptor)
15export default class SmsController {
16 constructor(
17 private readonly smsService: SmsService
18 ) {}
19
20 @Post('check-verification-code')
21 @UseGuards(JwtAuthenticationGuard)
22 async checkVerificationCode(@Req() request: RequestWithUser, @Body() verificationData: CheckVerificationCodeDto) {
23 if (request.user.isPhoneNumberConfirmed) {
24 throw new BadRequestException('Phone number already confirmed');
25 }
26 await this.smsService.confirmPhoneNumber(request.user.id, request.user.phoneNumber, verificationData.code);
27 }
28
29 // ...
30}Sending messages
The first step in sending messages through Twilio is choosing a phone number from the provided list.
We have an amount of money we can spend for free during the trial period in Twilio.
We also need to add the purchased number to our environment variables.
1import { Module } from '@nestjs/common';
2import { ConfigModule } from '@nestjs/config';
3import * as Joi from '@hapi/joi';
4
5@Module({
6 imports: [
7 ConfigModule.forRoot({
8 validationSchema: Joi.object({
9 TWILIO_SENDER_PHONE_NUMBER: Joi.string().required(),
10 // ...
11 })
12 }),
13 // ...
14 ],
15})
16export class AppModule {}1TWILIO_SENDER_PHONE_NUMBER="(818) 924-3975"
2# ...The last step is adding a new method to our SmsService:
1import { Injectable } from '@nestjs/common';
2import { Twilio } from 'twilio';
3
4@Injectable()
5export default class SmsService {
6 private twilioClient: Twilio;
7
8 // ...
9
10 async sendMessage(receiverPhoneNumber: string, message: string) {
11 const senderPhoneNumber = this.configService.get('TWILIO_SENDER_PHONE_NUMBER');
12
13 return this.twilioClient.messages
14 .create({ body: message, from: senderPhoneNumber, to: receiverPhoneNumber })
15 }
16}Using the above method results in sending SMS to the provided phone number.
Summary
In this article, we’ve used Twilio for implementing SMS messages. This included both validating the phone numbers of our users and sending messages. Although the trial period in Twilio has some limitations, we are free to experiment with the API. Therefore, feel free to look into more of the built-in features and explore.