As promised, this week we are going to build a JSON API with Ruby on Rails. Before we get started though let’s make sure everyone is setup and knows what we are building. First the tech requirements:
Ruby 2.6.3
Rails 6.0.0.rc2
PostgreSQL
Insomnia (for local API requests on OSX)
What We Are Building
We are going to build a light rewrite of the Basecamp 3 API in our lovely new JSON:API format. For the sake of time and simplicity we will be skipping the authentication workflow and instead will dive right into creating our data graph.
We will be referencing the guides and docs from Graphiti to build our project. In addition we are going to try to build this thing following BDD principles with RSpec.
Create The New Application
To create a new Rails API we are going to use the Graphiti template to generate the new project. We are going to call this app “Less Projects”. In the command below you will notice the -T flag which tells Rails to skip Test::Unit. Since we are using RSpec there is no need for the second testing library.
Projects
Projects will serve as our top level object. They are a container for all other resources.
Creating Projects
Graphiti and Rails provides us with some wonderful “generators” to get started.
Let’s step through these generated files quick. Graphiti just handled a lot of boilerplate for us. If you look at the ProjectsController (app/controllers/projects_controller.rb) you will see that all 5 of the basic CRUD actions are covered. You might also notice references to ProjectResource. This might be a little unfamiliar. It’s a construct that Graphiti introduces to perform most of the API behaviors. It handles fetching, mutating, filtering, pagination, and all kinds of useful things.
Next let’s take a look at the ProjectResource. Again these can do a lot of things, but let’s start with our bare bones. One of the key things a resource does is that it serializes (transforms) the Project model instances into JSON:API compliant JSON objects. In order to do that you must define what fields or “attributes” you want serialized. For our purposes these defaults (above) will work just fine.
Before we move into the tests let’s put them in a more typical hierarchy. RSpec has a fairly default folder hierarchy and moving our project into that will match more of what you will see out there in the wild. In this case it clearly marks our api specs as “request specs”.
Listing Projects
The test we will start with is listing projects. We can see that Graphiti was very helpful and wrote our first test for us. That’s pretty sweet! There are a few small things hidden from us. The first is jsonapi_get. This method is included by the Graphiti spec helper gem. It abstracts away some of the implementation details of the JSON:API spec, chiefly headers, to remove some test boilerplate and make tests easier to write.
The other thing it abstracts is the parsed response from the request. It’s hidden away in the d variable. This isn’t exactly named well so let’s use a different method that Graphiti supports and reads a little easier: “jsonapi_data”.
Alright, now let’s run our test and see if things work.
Woot! Our test already passes. Let’s take a moment to boot up the Insomnia app and take a look at our payloads. All we need to do is a little bit of setup first and start our server.
First let’s modify our default factory so that we can identify projects a little easier.
Next we setup some data for our API.
The server should now be running at http://localhost:3000. We can at last jump into Insomnia and see what our API returns for our first endpoint.
Step 1: Create a new Request
Step 2: Setup the default headers and Url
JSON:API Spec and Graphiti requires us to specify the format of the data for the server (Content-Type) and the format of data we expect to be returned (Accept). Both values will be application/vnd.api+json.
Step 3: Click “Send” and win!
Here is our data. You will notice there are 2 top level fields: “data” and “meta”. Data is where all of our primary data will be contained. The “meta” object is for more miscallenous types of data such as counts of records, pagination, etc. You might also notice that each “attributes” object has all of the fields we specified in our ProjectResource class. Not to shabby.
Fetching Single Projects
The next thing we want to make sure our clients can do is look up single projects. Again, like good devs let’s start with the test.
And now we run the test.
Just like before we find out our work was done for us and the tests pass. And just like before let’s go into Insomnia and add our request.
Pro Tip: Right click the previous request and click duplicate to start with a request with the required headers.
Creating Projects
Now that we have fetching of projects in place, let’s learn how to create new ones in our API. Like before, we will start with the test.
The are a few key things to point out here:
All payloads start with a data hash/object
type is required (this is the pluralized name of the object)
attributes is where all the non-relationship fields are contained
If you are not familiar with attributes_for don’t worry. This is just a convenient way to extract random attributes for a given factory. Let’s check this out in Insomnia so we can see a real world example.
Step 1: Create the request
Again we need to duplicate and rename the previous request. Also, let’s make sure to switch from a GET request to a POST request. When creating new resources over the API, the POST method is what the JSON:API spec requires.
Step 2: Create the POST body and Change the URL
We should make sure to change the url to reference /api/v1/projects. Think of this as the “root” url for projects. Making a GET request to this endpoint fetches projects and making a POST request creates them. Also make sure to change the request content to “JSON”.
You will also notice that much like our responses from the previous requests we are making use of the data field as the root element and passing our desired traits through the attributes hash. This is a pattern we will continue to re-use.
Step 3: Click “Send” and win!
Boom, just like that we have our new project. But what if we add some validations to projects and our request is invalid? Great question! Let’s make some quick alterations and see what happens. We start by writing a failing test.
And then we write the code to make the tests pass.
If we did everything properly then our tests should pass.
B E A utiful! Our test now passes. Let’s try to create a new project except we won’t give it a name. Since we just added the feature we know this should fail.
Here is what we we see if we try this in Insomnia:
From a client standpoint this is pretty nice. We are given a response that tells us what attribute are invalid and why.
Updating a Project
Now that we can create projects, let’s learn how to update existing ones in our API. Like before, we will start with the test.
Just like when we create a project, we pass our json with “data” as the top level attribute. Let’s check this out in Insomnia so we can see a real world example.
This time we will combine all the steps into 1.
And that’s all it takes to update the project.
Destroying a Project
The last operation that we need to support is destroying a project. Just like in all the other examples…we will start with a test.
If we run our tests, everything passes. That’s great, but what kind of response do we get from this request? Let’s dive back into Insomnia and add another request; just like we have previously.
The most notable thing in this request is that our response is quite different from the other ones. In this case only meta data is returned.
These 5 actions: list, fetch, update, create, and destroy are the main operations we will perform on each resource. Now let’s move onto some nested resources and see how Graphiti handles them.
TodoLists and TodoItems
Each project can have one or many todo lists. Each of these lists have todo items. Let’s use our generators again to create these resources.
And just like before let’s move our tests under spec/requests.
Now let’s alter our models and add the various relationships and default validations.
Next up, let’s alter our resources to add those same relations.
Let’s also make some changes to the new factories so that our tests are a bit more stable and the data a bit more random.
Alright, now that we have all of the foundations in place, let’s move into our new TodoList resource.
Listing TodoLists
In order to give Insomnia some data to display, seed some todo lists.
Then create and execute the new request in Insomnia
And of course we need to check our test.
If we run our test provided by Graphiti we see green!
Fetching TodoLists
Like before, let’s create and execute the a new request in Insomnia to fetch a single TodoList.
And of course we need to check our test.
If we run our test provided by Graphiti we see green!
Creating TodoList
Let’s create and execute the a new request in Insomnia to create a single TodoList.
And of course we need to write/fix our test that was autogenerated by Graphiti.
If we run our test provided we see green!
Updating TodoLists
Let’s create and execute the a new request in Insomnia to update a TodoList.
And of course we need to write/fix our test that was autogenerated by Graphiti.
If we run our test provided we see green!
Destroying TodoLists
Let’s create and execute the a new request in Insomnia to destroy a TodoList.
And of course we need to write/fix our test that was autogenerated by Graphiti.
If we run our test provided we see green!
Whew that’s two of the three resources covered. Time to rip through the TodoListItem and tease some of the content from part 2 of this article.
Listing TodoListItems
In order to give Insomnia some data to display, seed some todo list items.
Then create and execute the new request in Insomnia
And of course we need to check our test.
If we run our test provided by Graphiti we see green!
Fetching TodoListItems
Like before, let’s create and execute the a new request in Insomnia to fetch a single TodoListItem.
And of course we need to check our test.
If we run our test provided by Graphiti we see green!
Creating TodoListItems
Let’s create and execute the a new request in Insomnia to create a single TodoListItem.
And of course we need to write/fix our test that was autogenerated by Graphiti.
If we run our test provided we see green!
Updating TodoListItems
Let’s create and execute the a new request in Insomnia to update a TodoListItem.
And of course we need to write/fix our test that was autogenerated by Graphiti.
If we run our test provided we see green!
Destroying TodoListItems
Let’s create and execute the a new request in Insomnia to destroy a TodoListItem.
And of course we need to write/fix our test that was autogenerated by Graphiti.
If we run our test provided we see green!
That covers all of our resources! Thanks for hanging in there with me while we stepped through this together. If you fast-forwarded, scrolled through, or encountered issues you can view the full repo here for Part 1.
I cannot wait to get into part 2 and show you all where JSON:API and Graphiti really shine. I’ll be covering some slightly more advanced topics such as side-posting, side-loading, filtering, sorting, and pagination.