Caching API calls that return largely static data and be a great way to improve application performance AND save $$$ by limiting server requests.
For example, an e-commerce site with products might benefit greatly from caching API calls to fetch lists of those products and re-deploying when new items are added. Caching an API call means making the HTTP request when we statically generate our application pages, and storing the results of that request locally, like in a json file, to be served from our CDN. This prevents the user from having to make the HTTP request to where ever the server it lives on is and wait for the response every time they view a page of our app!
There are added security benefits to this approach as well - we're not exposing our API in the browser at all!
TransferState in Angular
For caching data, Angular provides a TransferState API as a way to cache responses from HTTP requests and put them in a statically generated page.
// my-service.service.ts
import { TransferState, makeStateKey } from '@angular/platform-browser';
constructor(private http: HttpClient, private ngState: TransferState) { }
getVillagers(): Observable<Villager[]> {
const villagersKey = makeStateKey('villagers');
const cachedResponse = this.ngState.get(villagersKey, null);
if (!cachedResponse) {
return this.http.get<Villager[]>('http://acnhapi.com/villagers').pipe(
tap((res) => this.ngState.set(villagersKey, res))
)
}
return of(cachedResponse);
}
There's quite a bit of setup work that goes into using it and configuring how to serve the application properly. (example here if you're curious)
Scully-flavored TransferState
I'm clearly a huge fan of Scully as a JAMstack tool, and their approach to caching is chefs kiss.
Scully has abstracted some logic around using TransferState to make it super simple for developers to cache API calls with their useScullyTransferState
method.
The useScullyTransferState
accepts 2 params, the key you want to store your data under, and an Observable of the original state of what you're working with. In the following example, our original state will be the GET request we're making with HTTPClient.
In my Animal Crossing Field guide application, I have a service file where I have all of my HTTP requests.
Here is my getVillagers
request that returns a list of all villagers in Animal Crossing New Horizons, and YIKES there's 391! This large amount of data I'm requesting is very static and is the perfect use-case for caching those requests + limiting calls to the free 3rd party API I'm using.
// my-service.service.ts
getVillagers(): Observable<Villager[]> {
return this.http.get<Villager[]>('http://acnhapi.com/villagers')
}
Let's use useScullyTransferState
to cache the results of this call. First, import TransferStateService
from Scully and inject it into our service.
// my-service.service.ts
import { TransferStateService } from '@scullyio/ng-lib';
...
constructor(
private http: HttpClient,
private transferState: TransferStateService
) { }
getVillagers(): Observable<Villager[]> {
this.transferState.useScullyTransferState(
'villagers',
this.http.get<Villager[]>('http://acnhapi.com/villagers')
)
}
Now, rerun ng build
, followed by npm run scully
. You may notice something happening in your terminal output. Every page you are statically generating with Scully that has an HTTP request using the TransferStateService is getting a data.json
file created for it.
Scully's doing a few really cool things for us.
- If we're just in development mode, (vs. serving our generated static files), Scully will treat the API call as normal, the HTTP request will execute every time.
- The magic happens when we serve our statically generated app files. When we run 'npm run scully' to generate our files, Scully will make the HTTP request for us then store the results in a data.json. This data.json file lives next to the index.html file in the directory of the page we generated, to be served from the CDN. Again, this prevents the user from having to make the HTTP request to where ever the server it lives on is and wait for the response!
To really be clear, any page statically generated by Scully that makes an HTTP request you've returned with the useScullyTransferState
will cache the response of that request by storing it in a data.json file that is served on your static page. ๐ ๐ ๐
Caveats
Before you go CACHE ALL THE THINGSSSSS, do consider how users are interacting with your application. If there's heavy modification of the data, like a to-do list, implementing API caching may not give you much in terms of performance boost or improved user experience.
Be aware that if you're using this approach, that same data.json file will be served until you generate a new build. If your API changes, new items are added, etc, those won't be reflected in the statically served data.json file until you run a new build. I call this out because if you're new to the JAMstack approach and don't have automated builds for every time your content (including data delivered by your API) changes, users may not be getting the latest data.
Top comments (7)
Thanks for this post Jennifer. When building, I am not seeing any data.json created for that route when running "npm run scully" Is there any config that needs to be enabled?
Hi Sumeet, I followed the steps mentioned in this article and the data.json did not get generated for me too.. were you able to make this work for you?
Hi Hussain,
It was long time ago, but I think I had a token interceptor where i was intercepting request and adding Auth token in the header and that was causing the issue for me. When building I used "isScullyRunning" to check and skip it by calling "next"
Hope it helps!
Jennifer, After adding the useScullyTransferState as per your article, it does not generate any data.json for the routes. is there anything more to be done apart from using useScullyTransferState?
thanks!
I guess 'useScullyTransferState' not yet documented on the docs. Happy to know that from here. Thanks.
Yep! That's why I wrote this post! I know the Scully team is working really hard - writing documentation is tough!
Wow, thanks Jennifer...