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 anull
(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
- During initial page load (
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 fetchisCacheStale
- Tells us if we need now dataloading
- Convert Nuxtāsstatus
into a boolean for conditional UI rendering (like a loading spinner)
This whole system works like this
- User visits the project page
- Nuxt calls
getCachedData
- If the cache is still valid, use cached data
- If not, fetch new data and cache it
- 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.