I’m currently working on a Portfolio with Nuxt and Vue3, for which I’m using a Neon PostgreSQL database to store information about my projects, and load them into cards in the ā€˜Projects’ page, nothing revolutionary. I noticed though, that I was making an API call every time I navigated to the projects page or refreshed it, which seemed wholly redundant. I’m simply not churning out new projects every 45 seconds.

So, I decided to learn how to cache data and check the cache lifespan to decide when to check for new projects. Here’s how I made the composable that makes API calls in the projects page:

1. The Cache Duration

No explanation needed - It’s how long we want to keep and use our cached data. I opted for 30 minutes

const CACHE_DURATION = 30 * 60 * 1000    // 30min in milliseconds

2. The Timestamp

I use this to track when we last fetched for new data. It’s a Vue ref so it’s reactive, and it’ll either be null (never fetched), or a number (timestamp).

const lastFetchTimeStamp = ref<number | null>(null)

3. Cache Checker Function

function getCachedData(key: string) {
  if (!lastFetchTimeStamp.value)
    return null
  const cacheAge = Date.now() - lastFetchTimeStamp.value
  if (cacheAge > CACHE_DURATION)
    return null
 
  const nuxt = useNuxtApp()
  return nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
}

This function will be called by Nuxt before making a new request. If it returns null, a fetch is triggered.

  • If timestamp is null, return a null (forcing a fetch)
  • Calculate how old our cache is (last fetch - now)
  • If cache has exceeded its lifespan (30min), return null (forcing a fetch)
  • If cache is still valid, ask Nuxt for the cached data
    • During initial page load (isHydrating), get it from the payload
    • Otherwise, get it from static data storage

4. Data Fetcher Function

const {
  data: projects,
  status,
  error,
  refresh,
} = useLazyAsyncData(
  'projects',
  async () => {
    try {
      const response = await fetch('/api/projects')
      if (!response.ok)
        throw new Error('Failed to fetch projects')
 
      lastFetchTimeStamp.value = Date.now()
      return response.json()
    }
    catch (e) {
      console.error('Error fetching projects:', e)
      throw e
    }
  },
  {
    server: true,          // Allow server-side rendering
    immediate: true,       // Fetch right away when possible
    default: () => [],     // Use empty array while loading
    transform: (data: Project[] | null) => data ?? [],  // Handle null data
    deep: true,            // Deep reactivity for nested objects
    getCachedData,         // Use our cache checking function
  },
)

This is where data is actually fetched, using Nuxt’s useLazyAsyncData composable.

  • First call getCachedData function
  • If it returns null, run the fetch function
  • fetch function gets data and updates the timestamp
  • The data is cached by Nuxt.

5. Helper functions

async function forceRefresh() {
  lastFetchTimeStamp.value = null  // Clear the timestamp
  return refresh()                 // Tell Nuxt to fetch fresh data
}
 
function isCacheStale() {
  if (!lastFetchTimeStamp.value)
    return true
  const cacheAge = Date.now() - lastFetchTimeStamp.value
  return cacheAge > CACHE_DURATION
}
 
const loading = computed(() => status.value === 'pending')
  • forceRefresh - Clears timestamp and forces a new fetch
  • isCacheStale - Tells us if we need now data
  • loading - Convert Nuxt’s status into a boolean for conditional UI rendering (like a loading spinner)

This whole system works like this

  1. User visits the project page
  2. Nuxt calls getCachedData
  3. If the cache is still valid, use cached data
  4. If not, fetch new data and cache it
  5. The projects section has a refresh button to force a refresh at any time.

To me personally, the Nuxt documentation felt quite lacking and I ended up having to ask Claude to explain a bunch of the usage pattern to me, so I thought why not leave a note here for myself as future reference, as well as anyone else who might stumble across it. Hope it helps.