Skip to content

Instantly share code, notes, and snippets.

@broox
Last active November 21, 2023 14:26
Show Gist options
  • Save broox/befb2e4a961a6d80d3d743e97925a67c to your computer and use it in GitHub Desktop.
Save broox/befb2e4a961a6d80d3d743e97925a67c to your computer and use it in GitHub Desktop.
Nuxt composable REST API Caching

I have a Nuxt 3 app that hits a rest API in order to display a paginated list of blog previews. Each preview can be tapped on to view the entire blog.

My app interfaces with 2 API endpoints.

  1. GET /api/v1/blogs returns a paginated listing of blogs.
  2. GET /api/v1/blog/:slug returns a single blog by an identifier.

Goals

Make as few server/API requests as possible, since much of the data can be easily cached on the client-side.

  1. If I load the first page of blogs and then navigate to page 2, it should load the second page of blogs on the client-side. If I navigate back to page 1, the client-side application should attempt to re-use the data that was previously fetched.
  2. If I tap on an individual blog from this listing, I should navigate to the individual blog page, but the app should not have to make an API request since the blog I'm tapping on was already loaded with the listing.
  3. Landing on any blog page, whether it be a listing or an individual blog, should make a server side API request to fetch the relevant data, display it, and cache it.
  4. Use as much built-in functionality as possible.
  5. Don't duplicate data in the cache.

Solution

I'm attempting to solve this with composables - useNuxtApp, useLazyFetch and the built in getCachedData() hook.

Goal #1 is easily solved with getCachedData(), the useNuxtApp().payload.data object, and a cache key that is specific to the relevant page/querystring.

#2 is a little trickier. To reuse the useNuxtApp().payload.data cache from the blog listing, I need to know the key that the individual blog lives within. To solve this, I'm writing the most recent listing's complex key into a custom pinia store with a more obvious key (e.g. mostRecentBlogs = blogs?page=23). Then, on the individual blog page, I can ask pinia for the key of the most recent blog listing, get its data, and see if my blog is in there via js array filtering. If it exists, return it instead of making an API request.

The rest feels solved and/or close.

Question

I'm just looking for thoughts on this approach. It solves the issue and seems incredibly performant, but the code feels a little more heavy handed than it needs to be.

Example files below 👇

// This is the composable used by the blog index page. The blog index page loads a
// list of blogs, truncates the text to display previews, and allows users to tap
// onto an individual preview to navigate to the blog entry / detail page
interface GetBlogsInput {
page?: Ref<number>
pageSize?: Ref<number>
search?: Ref<string>
tag?: Ref<string>
};
export default (input: GetBlogsInput) => {
const nuxtApp = useNuxtApp();
const cacheStore = useCacheStore(); // pinia store
const { public: { defaultPageSize } } = useRuntimeConfig();
const limit = computed(() => {
return input.pageSize?.value || defaultPageSize;
});
const offset = computed(() => {
const p = input.page?.value || 1;
return (p > 1) ? ((p - 1) * limit.value) : 0;
});
return useLazyFetch<BlogIndex>('/api/v1/blogs', {
getCachedData(key) {
// When a request is made to the API, `useFetch()` automatically caches the response data
// into a `payload.data` object with a given key. If we know that key, this function
// allows us to easily re-use recently fetched data without making another API request
// `getCachedData()` also seems to be the closest thing to a `before` hook on `useFetch`.
// As such, let's note the `key` of the current `useFetch()` request so that we can
// easily reuse its data.
// This will be used to look up a single blog entry from a list of blogs without
// having to make an extra API request.
cacheStore.set('blogs', key);
// If the listing data is cached, return it. If nothing is found, the API will be hit.
return nuxtApp.payload.data[key];
},
key: `blogs/${JSON.stringify(input)}`,
query: {
limit,
offset,
search: input.search,
tag: input.tag,
},
});
};
// This is the composable used by the individual blog entry page.
// It simply loads and displays a blog by slug.
export default (slug: string) => {
const nuxtApp = useNuxtApp();
const cacheStore = useCacheStore(); // pinia store
return useLazyFetch<Blog>(`/api/v1/blogs/${slug}`, {
getCachedData(key) {
// The `payload` cache is set automatically by `useFetch()` requests.
// This usually happens when landing directly on an individual blog.
// The server side request would fetch and store the blog into this
// object for use on the client-side. Return it if it exists.
const blogCachedInPayload = nuxtApp.payload.data[key];
if (blogCachedInPayload) {
return blogCachedInPayload;
}
// The blog index pages also call `useFetch()` and cache their own data in the `payload` object.
// To avoid making an additional request to the API after tapping on a blog from an index,
// we can try to find the individual blog in the `payload` cache. However, to do this, we
// need to know the key of the API request that fetched the listing of blogs. Look that up in pinia.
const cacheKey = cacheStore.get('blogs');
if (!cacheKey) {
return;
}
// `useNuxtData()` fetches data from the `payload` cache by key
const { data: blogs } = useNuxtData<BlogIndex>(cacheKey);
if (!blogs.value) {
return;
}
// We found a list of blogs by key, try to look up the relevant blog by its slug
const blogCachedInNuxtData = blogs.value.data.find(b => b.slug === slug);
if (blogCachedInNuxtData) {
return blogCachedInNuxtData;
}
// If nothing is found, we'll just make the request to the API
},
key: `blog/${slug}`,
});
};
// This is the custom pinia cache store that I use to simply note the complex cacheKey from the most recent listing request.
import { defineStore } from 'pinia';
type SimpleKey = 'blogs' | 'tags';
interface CacheKeys {
blogs?: string
tags?: string // or whatever other things i might want to cache
}
export const useCacheStore = defineStore('cache', () => {
const keys = ref<CacheKeys>({});
const get = (key: SimpleKey) => {
return keys.value[key];
}
const set = (simpleKey: SimpleKey, cacheKey: string) => {
keys.value[simpleKey] = cacheKey;
}
return { get, keys, set };
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment