Here is how we did it before, what we changed, and how it looks now. (Big thanks to jfrisella for leading this).
Previous API Calls
Here’s how we would do a basic post to one of our API endpoints.
For this example, we will be using ‘Users’ as the model, and show how it was done before the SDK. First (above), a model is defined, and requires an API route to be defined with it. In this case:
/api/route/ExampleUsers. Next, one of our Views is instantiated (you can ignore, or have a look at how it’s done, it’s not the focus of this post). Inside one of the event callbacks
.call() to hit the API. We tell it that it is a
POST, pass in our params, and sometimes pass in additions to the url.
This is our basic pattern for API calls on the front-end, and overall it works perfectly fine. It has several main drawbacks that made it worth our while to rework.
- Readability — When working through a bug or updating a feature, determining what the
.call()in the model is doing can take a minute to read through. The
.call()actually says a lot about the API call (whether it is create/update/get/delete), but when glancing at code, can be easy to misread. It would be nice if this was more semantic.
- Verbose — The model is defined in every View that it is used, and sometimes not clearly. So you may have to scroll up to double-check which API route is being used.
- Dispersed — We will centrally locate all API calls and routes, so if the route ever changes, it will just change once in the api.js file, instead of where every DataModel was instantiated
- Documentation — Not being centrally located makes finding, understanding and documenting each model a chore. The new API SDK will be housed in a single file that will be simply transformed to documentation
- Testability — Hand in hand with a dispersed, undocumented system, our goal of adding front-end tests to a code base without them looked daunting with the existing API structure. The new one provides a clear framework for testing
Notice that the point of this SDK is not necessarily saving lines of code (it did end up trimming a thousand something lines, but out of a very large code base). The primary objective was clean and readable API calls.
The New API SDK
The heart of this API are the jQuery
deferred object, and the
$.ajax interface. We will define the API using an IIFE and return it out to the global scope. (This can be attached as a separate api.js file or bundled up with the rest of your code base using
module.exports, which I won’t cover here). The basic pattern for this is similar to a jQuery plugin, shown here.
Here is the basic layout for a
POST, which we will be using to create our ‘User’.
.post() will take the API route, as well as the params required (either an object or array), as its arguments.
$ajax.post method is not our end goal, as notice that the returned object is
self. Next we will start attaching to the
self object, and use this
post() method to begin cleaning up our API calls.
To begin defining a section for our API, we will attach a new
Users function to
self and attach a
.create() function to the prototype of the
Users function as shown below.
Voila. Notice how the
.post() here receives the API route required, and takes the params passed in the
.create() method as it’s second argument.
Now we should have access to
new API.Users() anywhere in our code. (This same pattern is repeated for our
DELETE methods, which I will post at the end.)
Next, what does this look like when we actually make the call? Well, it looks really similar. A little cleaner in my opinion, but much more readable. Also, with this pattern we can skip instantiating the model in each View altogether:
Here’s the new call for creating a User in the View:
A little can go a long ways. Much clearer as to what is being done in
exampleFunction and a few less lines of code as well. Now here is a side-by-side comparison of the new vs. old.
We will continue this basic pattern for building out all of our CRUD operations, but this
.create() method only takes params. What if we want to have other methods on
User that alter the url? It’s very simple to handle these dynamic routes with just a slight tweak to the same pattern.
Dynamic API Routes
How do we approach the idea of having dynamic routes like
/api/route/exampleUsers/:exampleUserId? This is commonly used on
DELETEs for targeting an individual record. Here’s an example in action:
Now creating the
.update() call with a dynamic route that relies on adding the userId to the url:
Lastly, here’s how we would make that call and pass in the userId on the front-end:
Those two patterns, the
.update() shown above should cover the vast majority of use cases. I will link to the github repo with the full examples.
This process was one that had very satisfying benefits almost immediately.
- The API calls are much more readable both when they are called, and also in the central
- While not the focus, lots of code was trimmed, making it cleaner and easier to maintain in the future
- The bones for building out our new documentation and testing framework are all in place
In just a few short days, I had switched out all existing
.call() instances and upgraded to the new API SDK. We are still adding to the API as needed, and it has been a very high impact project across several facets of our workflow. Features, tweaks, and bug fixes are all benefiting from the clarifications made.
I hope you enjoyed, here is a link to the completed files, with full CRUD examples (I’ve removed our View code as the calls should be usable inside any View framework). Thanks!