Refactoring the Twilio integration for sending message

devil_!234@
3 min readMay 21, 2023

--

In this article, I will continue with a bit of refinement of my previous article(https://medium.com/@surajit.das0320/building-a-sms-api-at-scale-a7fe89a0830f) that I wrote on building a SMS API with an integration with Twilio.

Reason for refinement: —

  1. Clean code.
  2. Better error handling.
  3. More configuration to easily allow to scale.

Recap — https://medium.com/@surajit.das0320/building-a-sms-api-at-scale-a7fe89a0830f

This is how the current twilio send_sms functionality looks like. Lets call it twilio_sms_v1.py

import logging
from django.conf import settings
from twilio.rest import Client

from api.exceptions import TwilioException

logger = logging.getLogger(__name__)


def send_twilio_sms(message):
# Account SID from twilio.com/console
account_sid = settings.TWILIO_ACCOUNT_SID
# Auth Token from twilio.com/console
auth_token = settings.TWILIO_ACCOUNT_AUTH_TOKEN

client = Client(account_sid, auth_token)

try:
twilio_message = client.messages.create(
to=message.destination,
from_=message.source,
body=message.message)
logger.info(f"Twilio-SMS sent to - {message.destination}")
return twilio_message.sid
except Exception as e:
logger.exception(f"Twilio-SMS failed to - {message.destination}. Error: {e}")
raise TwilioException(e)

So, looking at the code, you can see there is a error.

  1. source — which is typically the sender number. Currently, it is getting from the message object. I am not sure if that’s correct because this functionality will be used by other services and we don’t necessarily need them to know the sender number configured in the sms API for sending the SMS.
  2. Generic exception handling — Bad bad idea. We are catching a very broad exception. Technically, it is a bad idea.
  3. Better logging to make traceability easy.
  4. Adding asserts. I will show you shortly what I mean.

Let’s do the refinement.

Firstly, I want to tackle the sender number which is the from_ . Keep in mind, we will need to reuse this functionality for multiple countries. So, we will have different numbers configured for sending sms. Could be just one twilio account for a start but the sender number has to change.

Let’s fix the settings file

settings.py

# TWILIO_SENDER_NUMBER_FOR_KE = ""
# TWILIO_SENDER_NUMBER_FOR_TZ = ""
# TWILIO_SENDER_NUMBER_FOR_UG = ""

TWILIO_SENDER_IDS = {
"KE": "",
"TZ": "",
"UG": "",
}
TWILIO_ACCOUNT_SID = ""
TWILIO_ACCOUNT_AUTH_TOKEN = ""

Ok, so what did we do here, instead of using a variable, we switched to a dict with the key being the country iso code.

Now, let’s fix the send_twilio_sms functionality.

twilio_sms_v2.py

import structlog
from typing import Dict, Optional

from django.conf import settings
from pydantic import BaseModel, ValidationError
from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client

from messaging.models import Message

logger = structlog.get_logger(__name__)
log_prefix = "Twilio-SMS"


class TwilioConfig(BaseModel):
account_sid: str
auth_token: str
sender_ids: Dict[str, str]


def send_twilio_sms(message: Message) -> Optional[str]:
try:
twilio_config = TwilioConfig(
account_sid=settings.TWILIO_ACCOUNT_SID,
auth_token=settings.TWILIO_ACCOUNT_AUTH_TOKEN,
sender_ids=settings.TWILIO_SENDER_IDS,
)
sender_id = twilio_config.sender_ids.get(message.country.code)
assert sender_id
client = Client(twilio_config.account_sid, twilio_config.auth_token)
twilio_message = client.messages.create(
to=message.destination, from_=sender_id, body=message.message
)
logger.info(f"{log_prefix} - SMS sent", destination=message.destination)
return twilio_message.sid
except (TwilioRestException, ValidationError) as e:
logger.error(f"{log_prefix} - SMS failed", destination=message.destination)
logger.debug(f"Exception details: {e}", destination=message.destination)
return None

Couple of changes -

  1. Pydantic model for Twilio credential validations.
  2. Structlog instead of normal logger. This is purely convenience and preference.
  3. Exception handling from generic exception block to specific exception e.g ValidationError and TwilioRestException.
  4. Log Prefix — This is key because if we are logging multiple log statement then log_prefix can be useful to come up with a cleaner prefix_name which can also be used to search the log in sentry or papertrail.

5. Instead of raising, I am returning None and this is because I dont think the caller function needs to do anything with the exception. Rather the caller function could check if the function returns something or None and then do something with it.

e.g —

def send_sms(message):
if message.country == "KE":
send_twilio_sms(message)

6. Bit of fix in the logger exception propagation because we don’t want to log sensitive keys in external logger service for security purpose. Hence, the considerations.

logger.error(f"{log_prefix} - SMS failed", destination=message.destination)
logger.debug(f"Exception details: {e}", destination=message.destination)

7. Type hints to make the code more efficient.

--

--

devil_!234@

Just a tinkerer who is passionate about software engineering. I share my mistakes and let others learn from my mistakes.