JavaScript Mapping Library
Last week I blogged about my first experience building a Cloudflare Worker serverless function. In that post, I built a simple serverless function that wrapped calls to the Pirate Weather API, a free and simple-to-use API for getting weather information. For today’s post, I thought I’d show how easy it is to add a bit of caching to the worker to help improve its performance. As with my last post, I’ve also got a video walkthrough of everything you watch instead. (Or read and watch, go crazy!)
In the last post, I shared the complete code of the Worker, but let me share it again:
// Lafayette, LAconst LAT = 30.22;const LNG = -92.02;export default { async fetch(request, env, ctx) { const APIKEY = env.PIRATE_KEY; let url = `https://api.pirateweather.net/forecast/$ {APIKEY}/$ {LAT},$ {LNG}`; let forecastResp = await fetch(url); let forecast = await forecastResp.json(); let data = { daily: forecast.daily.data, alerts: forecast.alerts } return new Response(JSON.stringify(data), { headers: { 'Content-Type':'application/json;charset=UTF-8' } }); },};
As a reminder, it does the following:
wrangler
.dev.vars
This all works reasonably well. On my local machine when I fire up the development server and hit it a few times, I see timings between 1 and 2 seconds. Here’s an example of how that looks:
Cloudflare Workers come with multiple different things it can integrate with on their system, including [Cloudflare KV], a key/value system with highly performant persistence. As a key/value system it resembles the Web Storage API quite a bit. You can store data by giving it a key and fetch the same thing back with the key. Also like Web Storage, complex values need to be JSON encoded and decoded.
You can, and should, check the Workers and KV docs for how they work together, but let me demonstrate how simple it is to get started.
First, you create a KV namespace. This can be done via the wrangler CLI and looks like so:
wrangler kv:namespace create <YOUR_NAMESPACE>
For my test, I’m using weather4 for the name (I’ve iterated on my demo a few times and wanted to keep the files separate), so I’ll make a namespace with the same name:
weather4
wrangler kv:namespace create weather4
When run at the CLI (note, if you don’t have wrangler installed globally, you can prefix the command with npx to use the command from your Worker project). This will output:
npx
Add the following to your configuration file in your kv_namespaces array:{ binding = "weather4", id = "b1342a22bcfd4af68f075223739025b3" }
The configuration file is wrangler.toml, and any scaffolded Worker project will include one. To add it, you can open the file and paste in this:
wrangler.toml
kv_namespaces = [{ binding = "weather4", id = "b1342a22bcfd4af68f075223739025b3" }]
If you fire up your Worker project again, however, you’ll get an error:
Luckily this is very clear and tells you exactly what to do. When you use KV in a Cloudflare Worker, they want you to create a development copy of the namespace. This can be done by literally rerunning the last command and adding --preview:
--preview
wrangler kv:namespace create weather4 --preview
This time it outputs:
{ binding = "weather4", preview_id = "29f5c2406dc24417bfd7bf6a79c2c5a7" }
And you then copy just preview_id to your wrangler.toml:
preview_id
kv_namespaces = [ { binding = "weather4", id = "b1342a22bcfd4af68f075223739025b3", preview_id = "29f5c2406dc24417bfd7bf6a79c2c5a7" }]
At this point, you can run the development server and not have any errors. (Hopefully.)
Using the cache involves two steps – reading from the cache to see if it exists and writing to the cache when the data is fetched. Before we add that, I first moved the actual API call into its own function:
async function getForecast(key,lat,lng) { let url = `https://api.pirateweather.net/forecast/$ {key}/$ {lat},$ {lng}`; let forecastResp = await fetch(url); return await forecastResp.json();}
Back in the main part of the function, I can check for the cache like so:
let forecast = await env.weather4.get('cache');
The API is async, hence the await keyword, and you access it via the env argument and via the namespace. Finally, you pass the key value. If the value isn’t in the cache, you’ll get a null result. I can modify my code like so:
await
env
let forecast = await env.weather4.get('cache');if(!forecast) { // todo} else forecast = JSON.parse(forecast);
Remember that we need to JSON encode complex values, so if it was in the cache, it was a JSON string. If we need to get the data, we just do that inside the if condition:
if
console.log('need to fetch, not in cache');let data = await getForecast(APIKEY,LAT,LNG);forecast = { created: new Date(), daily: data.daily.data, alerts: data.alerts}
Notice I’ve added a value, created, just to see when the data was cached.
created
Finally, the value needs to be actually cached, and here is where I got most happy with the API. To store the value, you use put, and as you can probably guess, you’ll pass a key and value. But you can also pass an expiration value! You can expire at a certain time, or after a certain number of seconds. Best of all, this is all automated. If you tell KV to expire at a time and that time has passed, when you get the value, it will return nothing again.
put
get
The smallest time you can cache for is 60 seconds. Obviously, for a weather forecast you would want to cache for hours, not seconds, but for testing purpose I’ll set it to 60 seconds:
await env.weather4.put('cache', JSON.stringify(forecast), { expirationTtl: 60 });
Also, note I’m running JSON.stringify before setting the value. And that’s it. After doing this, and hitting the API a few times, check out the difference in speed:
JSON.stringify
So with a few lines of code, and two commands run the CLI, the worker is now returning results in near instantaneous speed. I love it! Here’s the complete code:
// Lafayette, LAconst LAT = 30.22;const LNG = -92.02;async function getForecast(key,lat,lng) { let url = `https://api.pirateweather.net/forecast/$ {key}/$ {lat},$ {lng}`; let forecastResp = await fetch(url); return await forecastResp.json();}export default { async fetch(request, env, ctx) { const APIKEY = env.PIRATE_KEY; let forecast = await env.weather4.get('cache'); if(!forecast) { console.log('need to fetch, not in cache'); let data = await getForecast(APIKEY,LAT,LNG); forecast = { created: new Date(), daily: data.daily.data, alerts: data.alerts } await env.weather4.put('cache', JSON.stringify(forecast), { expirationTtl: 60 }); } else forecast = JSON.parse(forecast); return new Response(JSON.stringify(forecast), { headers: { 'Content-Type':'application/json;charset=UTF-8' } }); },};
You can watch a video of this below.
Raymond Camden
You must be logged in to post a comment.
This site uses Akismet to reduce spam. Learn how your comment data is processed.