Let’s Face It…
For those that write applications, processes or integrations with application programming interfaces (APIs) – third-party or developed in-house – we revel in the ease of well-documented, secure, discoverable RESTful APIs that operate swiftly and consistently. Yet while some products may afford us that luxury, we may be just as likely to find APIs that are anything but the ideal: slow, undocumented, inconsistent across functions/endpoints, and the list goes on…
Much as we’d like to ignore these APIs, we often cannot; our solution or product must still be developed, and we must still access the service to achieve the requirements asked of us. If you find yourself in this situation, as myself and others on our team have from time to time, you may want to keep the following points in mind to ensure you get the most out of your less-than-desirable technical integration.
Asking the Right Questions
Before you dive deep into the rabbit hole of technically-based workarounds, it’s worth asking yourself and your team:
Is it needed?
At some point, the debt accrued and work required to integrate an unoptimized API can outweigh the supposed benefit it was intended for. If this unoptimized service is intended to support one or more critical features or requirements of your app, it cannot be ignored. Yet if this service was mostly designed to provide enhanced functionality, or only acts as a “nice to have” for your users or process, building a proof-of-concept that doesn’t include the unoptimized API might be a worthy endeavor. While you may not be the final arbiter of determining whether the legacy system stays or goes, your users can weight the impact of performance when present, or missing functionality when absent, and so on via A/B testing the options.
Do other options exist?
The obvious question of if or how other products could accomplish what you’re trying to do is likely to come to mind. Less likely, and perhaps even more important, is to confirm that the way you’re CRUDing data on this system is the only option for fulfilling your requirements. It’s worth asking if any other APIs, documented or not, and/or methods (e.g. database) exist for accomplishing the same thing. One revelation in a recent project came in an email chain between myself and a few developers of a third-party service. I had spent months working on optimizing the SOAP API for their product, when a developer of the company noted briefly offhand that a JSON-based remote procedure call API existed. This (publicly) undocumented API was not only far simpler to work with, and anywhere from two to three times faster performance-wise, it was also what their front-end web tool utilized when making calls to the back-end. Though this discovery happened by accident, it only happened because of an already-opened communication channel between myself and the company, which brings us to the next point…
Let’s Chat
If the API is developed in-house, this may be as simple as a side conversation on Slack, or an email to the developer/development team. If it’s a third-party service, this may entail the product owner, project manager, etc. reaching out on your behalf in a more formal request. In either case, this relationship could be essential throughout the development lifecycle of your product, and beyond. Asking direct questions early and often will save many potential headaches, and could include the likes of:
- Do you have documentation available for your API?
- Can you provide samples/examples of how most customers interact with your service?
- Do you have other APIs, options, etc. for performing the same actions?
- Can we discuss adding feature X to your API, or as an API endpoint?
- Would it be possible to add information Y to this existing API endpoint?
In short, use the company and their support channels to your advantage as, especially in small operations, you may not know what your options are until you ask, and ask again…and then some more. In truth, you cannot fix broken organizational culture, and if the company is a product of this (e.g. antiquated development lifecycles, unwillingness to refine their product) it will be apparent. Yet if the company’s offering is simply a bit behind the times, and the folks you’re working with are willing and able to communicate and improve upon their existing offering, you may gain an active partner in building your solution.
Of course, if you’ve gone through all of this, and must still integrate an unoptimized API with little/no changes made to make it better, it’s time to break out your development toolkit.
Client-Side Optimizations
When in doubt, a hint of JavaScript and some user experience-oriented paradigms may be just enough to mask the trouble of an unoptimized API.
Using a Persistence Layer
localStorage
and sessionStorage
in JavaScript – you’ll probably want to investigate these, select one and use it as your primary strategy in managing data for your app. Other than your initial data load, this abstraction makes getting information become a call to the client-side:
var existingData = JSON.parse(sessionStorage.getItem("myAppData"));
And a simple line after create/update/delete actions to ensure the latest state of your data gets saved:
sessionStorage.setItem("myAppData", JSON.stringify(newData));
Suitable for vanilla JavaScript, this can also be worked into state management libraries (e.g. Redux, MobX for React, Vuex for Vue.js) if you choose to embark on a more modern front-end architecture. This will also save significant time in (re)loading the same information across various pages in your app, or even additional applications amongst an app suite, where bits of information are shared between. Persisting data for the life of your user’s session, across browser tabs, etc. is imperative for reducing needless overhead at the client end.
Preloading Information
Do you expect users to work through a modal, other pages, etc. prior to reaching the point of requiring the unoptimized service? You may be able to silently load and store the content as they navigate through your app, having it ready for the user once the direct task is needed. For services that pay an initial penalty in startup, this allows you to silently work through the startup cost without penalizing the user with a lengthy wait. Even if the the entire preloading call cannot be hidden from the user, simple loading icons can inform the user that something is occurring, and set an expectation of a brief waiting period before content is displayed. And speaking of that…
Set Firm Expectations, If Needed
If your unoptimized API call, or series of calls, is going to take longer than the rest of your application’s standard loading time, be sure to let your users know. As a general rule, take the average time for the request and multiply by two for an “up to” value. This way, users receive a clear, direct indicator both visually and textually that content may take some time to load, and could take up to X seconds depending on the circumstances. This is the model we use for Wharton’s Forwarding application:
Lazy Loading
You can additionally consider deferring the calls to obtain information from the unoptimized API until it’s absolutely required, perhaps acting as a complementary technique to preloading. This is a particularly common strategy used for loading media (images, videos), but is similarly implemented for textual data like comments, reviews, etc.
Server-Side and Other Optimizations
Beyond the client-side, and particularly useful for cases when the client isn’t involved (e.g. background jobs and tasks), server-side optimizations can offer some advantages as well.
Wrap It
If your unoptimized service requires dealing with de/serializing XML, unstructured or obtuse data formats unlike the rest of your APIs, highly consider wrapping your API calls server-side. Though this should probably be done for other reasons as well, such as cases where sensitive credentials or tokens must be injected, this allows you to ensure your client-side app, or other parts of your service, interact with familiar objects and abstractions (e.g. JSON) like your other APIs, and aren’t riddled with messy logic to handle the edge case API.
Parallelizing, and Less is More
If the typical GET request to your unoptimized API entails a myriad of calls, themselves which each may take a considerable amount of time, consider using parallelized calls to break up the work. This is especially relevant for services which break up their information across numerous endpoints, e.g. requiring three or four API calls to achieve what some services provide in one. You may also think back to the “Is it needed?” question and consider what information you must query or change, as opposed to what you’d like to. Even if your unoptimized service only requires one long API call, examine what you might be able to take care of programmatically while that call is being processed. In scenarios where you’re wrapping the unoptimized API and – in that same wrapped API – calling other services, this can be highly beneficial.
Cache What You Can, When You Can
Though this will undoubtedly be an added layer of complexity, a variety of techniques in the caching realm can be used to improve user experience, and enhance performance on the surface of relying upon your unoptimized API.
Requests to your wrapped API could instead be factored into a message queueing (e.g. Redis) and task queueing (e.g. Celery) model, where messages are sent to a queue that are picked up by task workers that eventually take care of the work. All the while, users were instantly notified of the change that will, rather than did, take place, and the less-than-desirable performance of the API is left to a back-end job to handle without the user’s knowledge.
Alternatively, your database model itself could become a replica of the data stored and handled by this unoptimized API. This means you’re taking over a fair bit of functionality that the service itself should handle, and now bear the responsibility of keeping this in sync such as via back-end jobs that regularly run. This provides a model-layer level of data representation familiar to most developers.
A final, and less-often considered strategy, is to trust the client-side persistence layer to maintain some state for you in cases of expensive (time-wise) calls, in the scenario of wrapping your unoptimized API. e.g. If every PUT request to the unoptimized API requires a GET prior to this, and the GET takes considerable time, does the client have this data already? If so, can your wrapped API trust and consume the client’s data to use as an accurate, up-to-date representation to send to the unoptimized service, without needing to make another expensive call?
Final Thoughts
Optimizing what is and seems otherwise unoptimizable isn’t impossible, though it requires asking some critical questions, and exploring a number of different technical avenues. Hopefully this leads you on the path towards a solution unencumbered by an API or vendor gone rogue!