Building an authenticated web API wrapper with Crystal
Crystal is an up-and-coming language very similar to Ruby, but compiled. It's still very young, and it's changing all the time, but I've been having some fun playing around with it. I started writing an API wrapper for Tumblr (which has basically become my "Hello World" now...). Unfortunately, the documentation for some parts of Crystal doesn't yet exist, so I learned a ton from the community. Because there aren't really any tutorials on how to do this sort of thing yet, I'd like to share what I've learned.
Big special thanks to Asterite and the group Gitter chat for all of their help!
Preface
This tutorial is for beginner / moderate skill level with programming. If you know Ruby or even Python, you'll find this super-easy. If you're stuck on something, send me a message and I'll try to clear it up!
Installing
Because it's very possible that you've just heard of Crystal for the first time, you're probably going to need to install the library. Detailed instructions on how to do this are available on the Crystal docs website.
Note: As of September 2016, Crystal does not have strategy for installing on Windows computers. If you don't have access to a *nix or MacOS machine, consider using a virtual machine.
Creating a Crystal Project
Once installed, it's easy to create a new Crystal project. Let's create ours and name it twitter-wrapper-test
.
crystal init lib twitter-wrapper-test
This will create a few files we'll use to bootstrap our project for use with Shards. Speaking of Shards... what are they?
Getting Started with Shards
Shards is Crystal's dependency manager, similar to Rubygems, NPM, and NuGet. Crystalshards is decentralized in a way, where adding a dependency is basically just adding a git URI. As of now, however, almost all repositories are hosted on GitHub. You can search them with Fatih Kadir Akın's excellent Crystalshards.xyz.
Let's open up our shard.yml
(which is similar to a Ruby project's Gemfile
).
name: twitter-wrapper-test
version: 0.1.0
authors:
- doomy <myemail@email.com>
license: MIT
When a project is created, it initializes some basic information like the license, authors, and version. Change these to your liking. For this project, we don't need any dependencies. Everything we need is built into the standard library! (Awesome, right?)
Getting started with Twitter
Although I'm using Twitter, feel free to follow along with another service as you see fit. Keep in mind, however, that I'm using OAuth 1.0 in this tutorial. You can use 2.0 with Crystal, but the process is a bit different.
Anyways, let's create a new Twitter application. Sign in or create your Twitter account, and then click here to generate a new app. Don't worry about the callback URL for now, we don't need it. Once that's finished, go to your new application's page, and click the "Keys and Access Tokens" tab. At the bottom, click "Create my access token". Great! This will create everything we need to test our API wrapper - a Consumer Key
, Consumer Secret
, Access Token Secret
, and Access Token
.
Setting up some environment variables
These keys we've created are extremely sensitive and should be treated as you would a password. This means that hardcoding them in our new Crystal application is a huge no-no. Because we're using a *nix system, we can set an environment variable. Open up your shell's .rc
file (in most cases, ~/.bashrc
) and append the following lines:
Where your-key-here
is replaced with the corresponding value from your Twitter app. Save and close your editor, and then open up a new terminal, or type source ~/.[your shell]rc
(most likely source ~/.bashrc
).
Do we have to name our variables
TWITTER_CONSUMER_KEY
?
No, not really. You can name these variables anything you like, but TWITTER_CONSUMER_KEY
offers a lot of clarity!
Building our wrapper
Time to do some coding. Open up our src/twitter-wrapper-test/
directory, and create a new file called client.cr
. Before we write any code, let's explain how and why we're going to structure the project like we are.
Static Classes vs. Instantiation
We're going to instantiate the client
class in order to perform requests. Although we could definitely use a static
class, it would have to be stateful and remember OAuth credentials. Stateful static classes aren't very clear when implemented, so we're going to simply instatiate a new client
object.
Setting up an HTTP object with OAuth
Crystal provides OAuth support in its standard library. Neat! Let's open up our client.cr
file and add a few require
statements. Let's also make a module
that's the same name as our library. (Notice that our init
automatically created Twitter::Wrapper::Test
because of the dashes. This actually should probably be TwitterWrapperTest
, so feel free to change all occurrences if you'd like. For simplicity's sake, I'll just use what it generated).
end
Inside our module, we're going to create a new client class, with an initialize.
=
# create a new client with oauth support
# create our OAuth consumer and token with the built in library!
consumer = OAuth::Consumer.new(Host, consumer_key, consumer_secret)
token = OAuth::AccessToken.new(oauth_token, oauth_token_secret)
# create an instance of the HTTP client using HTTPS
@http_client = HTTP::Client.new(Host, tls: true, port: 443)
# use the `authenticate` method on our consumer variable to authenticate our HTTP client with each request. Neat!
consumer.authenticate(@http_client, token)
end
end
And that's just about it! One of the most painless OAuth implementations in history :)
Getting information from an endpoint
Twitter provides multiple endpoints for fetching JSON data. For this tutorial, we're just going to search for some tweets. You can read more about it's specification here. Twitter provides some awesome API docs.
In our client, let's add a new method called tweet_search
.
= get(, { => query})
end
response
Simple enough, right? Well, if we tried to run this right now, we'd get a compiler error because our get
method doesn't yet exist. Let's create that, and a few other useful methods in our client class.
# generic function for getting JSON
private
# add parameters to our string
path += unless params.empty?
# finally, get our response
response = @http_client.get(path)
# handle the response properly and check for errors
handle_response(response)
end
private
case response.status_code
when 200..299
response.body
else
raise
end
end
# returns a URL encoded string used for query parameters
private
HTTP::Params.build do
params.each do
form_builder.add(key, value)
end
end
end
These methods are pretty self explanatory. I also have to admit, I almost entirely ripped these from sferik's source code. There's some great examples in that repo if you like reading source. Unfortunately, there's no documentation for it yet :(
Testing
Now that we have everything we need to make a simple request, let's see if our library works.
Open up src/twitter-wrapper-test.cr
. Let's write some code in the module block.
Note: Usually, you're NEVER going to want to write app-specific code inside of a
lib
package. Currently, there is no REPL in Crystal, so projects can't really be tested without making another Crystal application that consumes your custom library. In order to avoid doing this for simplicity sake, we can write stuff directly in ourtwitter-wrapper-test.cr
file. Just remember to delete it before sharing!
client = Client.new(
,
,
,
)
puts client.tweet_search()
Basically, we're creating a new client with our OAuth environment variables, and then searching for tweets with the string "@crystal-lang". Let's test our application.
crystal src/twitter-wrapper-test.cr
If all goes to plan, we should get a huge strin of JSON. This means our API authentication is working properly.
Deseralizing JSON with Crystal
Crystal provides some awesome, magical ways to deseralize JSON easily. So let's get started! Create a new file, src/twitter-wrapper-test/tweet.cr
.
We need to deserialize JSON that looks like this (scroll down to the bottom for JSON examples). There's a LOT of different properties for each tweet. Let's just keep it basic and parse the text
, id
, and user
properties.
In our tweet.cr
...
JSON.mapping(
text: {type: String},
id: {type: Int64},
user: {type: User}
)
end
end
The JSON.mapping
macro is an easy way for Crystal to figure out what properties of an object will be accessible, and how to deserialize/serialize the object to and from JSON.
You might notice that our user
property has a type that isn't already defined. We need to make a new User
class. Create a new file as src/twitter-wrapper-test/user.cr
. Again, there's a ton of properties given to us over JSON, so let's just take a few - id
, description
, and screen_name
.
In our user.cr
...
JSON.mapping(
description: {type: String},
name: {type: String, key: },
id: {type: Int64}
)
end
end
For our name
property, I passed in a key
parameter with the value of "screen_name". You can use this whenever your property name doesn't match up with the JSON
Finishing Up
Let's reopen our client.cr
file so we can use our new JSON enabled classes. At the end of our tweet_search
method, add the following:
return Array(Tweet).from_json(response, root: )
This will tell Crystal to use our JSON mappings to get an Array of Tweet objects in the root "statuses" (as defined in our JSON).
Let's switch back to our twitter-wrapper-test.cr
file and replace the puts ...
line with the following:
tweets = client.tweet_search()
tweets.each do
puts
end
And run crystal src/twitter-wrapper-test.cr
We should get this!
GaryAsh1969 - @burgerbecky i’ve been tracking this https://t.co/dMAPThdMuH since using Ruby
ysbaddaden - I uploaded the long overdue FreeBSD tarballs for Crystal 0.19.0 up to 0.19.2 https://t.co/vTACMU98Mv
piedoomy - RT @CrystalLanguage: Fund Crystal and help it become production-ready! https://t.co/AeBEGvw2TW
MattStudies - Great post from the Crystal Team https://t.co/OgczM0lcEN
Benchmarks are so frustratingly hard.
versiontracker - Crystal 0.19.2 released. https://t.co/4NXtqAh1Lc #compiler #ruby #developers #programming_language #c https://t.co/OrZxr8Rxwv
Cool.
For the future & Conclusion
This is a very simple wrapper, and it doesn't implement everything Twitter has to offer. Feel free to fully implement your client and push it to GitHub as a shard!
Crystal is constantly changing, so if you notice an issue with any of this code, please let me know so I can update it. I am also just learning Crystal, so if you have an idea on how to do something better, please let me know.
Donate
In lieu of donating to me, please consider funding Crystal on Bountysource so they can become production ready!
Also, have my this Crystal Cat I made.