Skip to main content

Fetch, Validate, and Cache an API Response

A cache, a TTL, and a validation step are usually three separate systems that all have to independently agree on what "valid" and "fresh" mean. Here they're one object: it fetches, checks the shape of what it got back, and remembers — so calling it twice in a row doesn't mean two network requests.

One honest note before the code: Http isn't loaded in this browser sandbox (see the Http library page), so simulated_fetch() below stands in for await Http.fetch(...) — same response shape, no real network call. Swapping it for the real thing in a host that has Http loaded is a one-line change; nothing else in the recipe cares which one it's talking to.

Fetch, Then Validate

step1.stof
fn simulated_fetch() -> map {
  // stands in for `await Http.fetch(...)`
  map(("status", 200), ("text", '{"rate": 1.08}'))
}

#[type]
Rate: {
  #[schema((target_value: float): bool => target_value > 0)]
  float rate: 1.0
}

#[main]
fn main() {
  const resp = self.simulated_fetch();

  const parsed = new {};
  parse(resp.get("text"), parsed, "json");

  pln(<Rate>.schemafy(parsed), parsed.rate);
}
Output

Adding the Cache

A fetch_count here just proves the point — in a real version it wouldn't exist, but it's the difference between claiming the cache works and actually watching it:

cache.stof
#[type]
Rate: {
  #[schema((target_value: float): bool => target_value > 0)]
  float rate: 1.0
}

cache: {
  fetched: 0ms
  data: null
  fetch_count: 0

  fn simulated_fetch() -> map {
      self.fetch_count += 1;
      map(("status", 200), ("text", '{"rate": 1.08}'))
  }

  fn stale() -> bool {
      self.data == null || Time.diff(self.fetched) > 5min
  }

  fn get() -> obj {
      if (self.stale()) {
          const resp = self.simulated_fetch();

          const parsed = new {};
          parse(resp.get("text"), parsed, "json");

          drop(self.data); self.data = null;
          if (<Rate>.schemafy(parsed)) self.data = parsed;
          else drop(parsed);
          self.fetched = Time.now();
      }
      self.data
  }
}

#[main]
fn main() {
  pln(self.cache.get().rate);
  pln(self.cache.get().rate);
  pln(self.cache.fetch_count);
}
Output

fetch_count stays at 1 even after calling get() twice — the second call found valid, recent data sitting right there and skipped the fetch entirely. root.Rate reaches the schema from inside cache, the same absolute-path mechanism from The Document Graph — cache doesn't need to know or care where the validation rule actually lives.