Send SMS messages with Crystal and Twilio

Phil Nash - Jun 26 '17 - - Dev Community

Crystal is a young programming language with a whole load of potential and it's been a lot of fun playing around with it over the past few weeks. I want to share with you how easy it is to send SMS messages with Twilio using Crystal.

The Crystal project started in September 2012 and is still going strong. The language hasn't reached version 1 yet, but there are many things about it to get excited about. The goals of the language are listed front and centre on the Crystal site, but briefly, it is a type-inferred, compiled language with Ruby-like syntax that is blazingly fast. To get started, let's take a look at a simple Crystal program:

def fib(n)
  if n <= 1
    1
  else
    fib(n - 1)   fib(n - 2)
  end
end

time = Time.now
puts fib(42)
puts Time.now - time
Enter fullscreen mode Exit fullscreen mode

If you've used Ruby before, then this will be instantly recognisable as a function to calculate the nth Fibonacci number. It's so recognisable that if you were to run this with Ruby the program would run correctly. Of course you can run it with Crystal too.

Running the program with Ruby produces the answer in 32 seconds, running with Crystal took 1.7 seconds

Pretty amazing, right? And Crystal is orders of magnitude quicker than Ruby in this test. Of course, the Fibonacci sequence is a terrible benchmark and this isn't exactly a fair test either, but it's interesting to see the similarities. As you start to dig into Crystal, however, the differences between this language and Ruby start to emerge.

Let's do that now and make some calls to the Twilio API using Crystal.

Getting started with Crystal

To use Crystal, you'll first need to install the compiler. For Linux, follow the installation instructions in the Crystal docs. If you're on a Mac you can install Crystal with homebrew with the following:

$ brew update
$ brew install crystal-lang
Enter fullscreen mode Exit fullscreen mode

Sadly, Crystal doesn't currently compile on Windows operating systems.

You can check that Crystal is successfully installed by running:

$ crystal --version
Crystal 0.19.4 (2016-10-21)
Enter fullscreen mode Exit fullscreen mode

We need a Twilio account for the next part, so if you haven't got an account, sign up for free here. We will also need a Twilio number that can send SMS messages.

Time to make some API calls with Crystal!

Sending an SMS message with Crystal

Normally when I write about sending an SMS I would point you towards our official helper libraries. As Crystal is still relatively new, there is no such library, so we're going to have to roll up our sleeves and investigate the HTTP module.

Create a new Crystal file named sms.cr:

$ touch sms.cr
Enter fullscreen mode Exit fullscreen mode

Open the file and start by requiring the http/client class from the standard library. Start a new function for sending SMS messages using Ruby's familiar def syntax. We'll need three arguments; a number to send the message to, the number we're sending from and the body of the message.

require "http/client"
def send_sms(to, from, body)

end
Enter fullscreen mode Exit fullscreen mode

Setting up the HTTP Client

Initialise an HTTP::Client with the base URL of the API the port number and whether the application uses TLS. We can then pass a block to this initialiser and the client will be closed once we are done with it.

require "http/client"
def send_sms(to, from, body)
  HTTP::Client.new("api.twilio.com", 443, true) do |client|

  end
end
Enter fullscreen mode Exit fullscreen mode

Now that we have our client, we need to add our authentication header using our Twilio Account SID and Auth Token as the username and password. I'm keeping my credentials as environment variables so that I don't accidentally share them publicly. You can set environment variables on the command line with the export command.

$ export TWILIO_ACCOUNT_SID=AC123456789abcdef
$ export TWILIO_AUTH_TOKEN=1q2w3e4r5t6y7u8i9op0
Enter fullscreen mode Exit fullscreen mode

Use the basic_auth method on the HTTP::Client instance to authenticate the client.

require "http/client"
def send_sms(to, from, body)
  HTTP::Client.new("api.twilio.com", 443, true) do |client|
    client.basic_auth(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
  end
end
Enter fullscreen mode Exit fullscreen mode

Making the HTTP request

Now we can make our request. To send an SMS message we need to make a POST request to the Messages resource. We build the URL using our Account SID. We need to send the properties of our message as application/x-www-form-urlencoded parameters, and we can use Crystal's built in post_form method for this:

require "http/client"
def send_sms(to, from, body)
  HTTP::Client.new("api.twilio.com", 443, true) do |client|
    client.basic_auth(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
    response = client.post_form("/2010-04-01/Accounts/#{ENV["TWILIO_ACCOUNT_SID"]}/Messages.json", {
      "To"   => to,
      "From" => from,
      "Body" => body,
    })
  end
end
Enter fullscreen mode Exit fullscreen mode

We could now call this function and send our first SMS message from Crystal. But we would only know whether it was a success once we received the message. Let's do a little more work to check whether the API call was successful and if so print out the message SID otherwise print out the error message.

Handling the response

Include the JSON module from the standard library and parse the response body. If it's a success we can look for the SID otherwise the error response will have a message field to tell us what went wrong.

require "http/client"
require "json"
def send_sms(to, from, body)
  HTTP::Client.new("api.twilio.com", 443, true) do |client|
    client.basic_auth(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
    response = client.post_form("/2010-04-01/Accounts/#{ENV["TWILIO_ACCOUNT_SID"]}/Messages.json", {
      "To"   => to,
      "From" => from,
      "Body" => body,
    })
    result = JSON.parse(response.body)
    if response.success?
      puts result["sid"]
    else
      puts result["message"]
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now add one final line to sms.cr to call the method with your own phone number in the to position, a Twilio number in the from position and your SMS message.

# sms.cr
def send_sms(to, from, body)
  # function code
end

send_sms(ENV["MY_NUMBER"], ENV["MY_TWILIO_NUMBER"], "Hello from Crystal!")
Enter fullscreen mode Exit fullscreen mode

Compiling and running

Compile and run the function and celebrate your first SMS sent from Crystal!

$ crystal sms.cr
SMadca071a5bab4848892d9f24863e99e6
Enter fullscreen mode Exit fullscreen mode

The crystal command compiles and runs the file you pass to it. You can also build a fully optimised executable that's ready for production with the build command.

$ crystal build sms.cr --release
$ ./sms
SMadca071a5bab4848892d9f24863e99e6
Enter fullscreen mode Exit fullscreen mode

We've sent our first message, but there's more to learn before we finish this chapter. JSON parsing using the JSON.parse method is fine, but you'll remember that I described Crystal as type-inferred at the start of the post. Parsing arbitrary data structures like JSON in a typed language can be awkward and require you to cast your properties to the expected type before using them. As it happens, the type of each property parsed using JSON.parse is JSON::Any and when we call puts with the object it is cast under the hood to a string with to_s.

Improved JSON parsing

Instead of using this JSON::Any type, we can actually tell Crystal's JSON parser what to look for with a JSON mapping. This will have the effect of being "easy, type-safe and efficient" according to the documentation.

Open up sms.cr again and add simple mappings for our message and error objects.

# sms.cr
class Message
  JSON.mapping(
    sid: String,
    body: String
  )
end

class Error
  JSON.mapping(
    message: String,
    status: Int32
  )
end
Enter fullscreen mode Exit fullscreen mode

There are more fields available on each of those objects, but we are only interested in those for now. Now, instead of using JSON.parse we can use the from_json constructor for each of our Message and Error classes. The mapping creates instance variables, getters and setters for the fields we define so we can use dot notation to access them.

def send_message(to, from, body)
  client = HTTP::Client.new("api.twilio.com", 443, true) do |client|
    client.basic_auth(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
    response = client.post_form("/2010-04-01/Accounts/#{ENV["TWILIO_ACCOUNT_SID"]}/Messages.json", {
      "To"   => to,
      "From" => from,
      "Body" => body,
    })
    if response.success?
      message = Message.from_json(response.body)
      puts message.sid, message.body
    else
      error = Error.from_json(response.body)
      puts error.status, error.message
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

If you run the file with crystal sms.cr again you will receive your message and be safe in the knowledge that your Message object is now correctly typed and efficiently parsed from the JSON response.

Crystal is pretty cool

In my early explorations with Crystal I found it as pleasurable to write as Ruby with the addition of type safety and speed. In this post we've seen how easy it is to use Crystal to make HTTP requests against the Twilio API and send SMS messages. If you want to see the full code, check out this repository on GitHub.

If you like the look of Crystal you can learn more about it at the official site and by checking out the documentation. There are a bunch of interesting projects listed on the awesome-crystal repo and if you're coming at this from Ruby, like me, take a read through the Crystal for Rubyists book.

What do you think about Crystal? Does it look like a promising language to you? Let me know your thoughts in the comments below or drop me an email or message on Twitter.


Send SMS messages with Crystal and Twilio was originally published on the Twilio blog on November 3, 2016.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .