Command Palette
Search for a command to run...

Caching

Cache paginated Notra post data safely, including sorting and invalidation patterns.

how-to · intermediate · 10m
s'applique à notra-api, typescript, react, nextjs
par Dominik K. ·

When caching Notra post data, include every query input that changes the result set in the cache key.

For list posts, that usually means:

  • page
  • limit
  • sort
  • status
  • contentType

Cache key design

Treat each unique list query as its own cache entry.

type ListPostsParams = {  page?: number;  limit?: number;  sort?: "asc" | "desc";  status?: Array<"draft" | "published">;  contentType?: Array<    "changelog" | "linkedin_post" | "twitter_post" | "blog_post"  >;};function createPostsCacheKey(params: ListPostsParams) {  return [    "notra",    "posts",    params.page ?? 1,    params.limit ?? 10,    params.sort ?? "desc",    [...(params.status ?? [])].sort().join(","),    [...(params.contentType ?? [])].sort().join(","),  ] as const;}

Sorting array filters before building the key helps avoid duplicate cache entries for equivalent queries.

Use separate caches for:

  • post lists, keyed by pagination and filters
  • individual posts, keyed by postId

This keeps list pages independent while still letting you refresh a single post after PATCH /v1/posts/{postId}.

const listKey = createPostsCacheKey({  page: 2,  limit: 20,  sort: "desc",  status: ["published"],  contentType: ["blog_post"],});const postKey = ["notra", "post", "post_abc"] as const;

Invalidation rules

Invalidate caches whenever the underlying list order or membership can change.

After a post update

After PATCH /v1/posts/{postId} succeeds:

  • invalidate the individual post cache for that postId
  • invalidate all cached post lists, because title, status, and updated content can affect what users should see

After a new generated post becomes available

When a generation job finishes and a new post is created:

  • invalidate all cached post lists
  • optionally prefetch page 1 again if your UI shows newest posts first with sort=desc

After a delete

After deleting a post:

  • remove the individual post cache
  • invalidate all cached post lists so pagination totals and offsets stay correct

Example with TanStack Query

import {  QueryClient,  useMutation,  useQuery,  useQueryClient,} from "@tanstack/react-query";function usePosts(params: ListPostsParams) {  return useQuery({    queryKey: createPostsCacheKey(params),    queryFn: async () => {      const search = new URLSearchParams({        page: String(params.page ?? 1),        limit: String(params.limit ?? 10),        sort: params.sort ?? "desc",      });      for (const status of params.status ?? []) search.append("status", status);      for (const type of params.contentType ?? []) {        search.append("contentType", type);      }      const response = await fetch(        `https://api.usenotra.com/v1/posts?${search.toString()}`,        {          headers: {            Authorization: `Bearer ${process.env.NOTRA_API_KEY}`,          },        }      );      if (!response.ok) throw new Error("Failed to fetch posts");      return response.json();    },  });}function useUpdatePost() {  const queryClient = useQueryClient();  return useMutation({    mutationFn: async ({      postId,      updates,    }: {      postId: string;      updates: { title?: string; markdown?: string; status?: "draft" | "published" };    }) => {      const response = await fetch(`https://api.usenotra.com/v1/posts/${postId}`, {        method: "PATCH",        headers: {          "Content-Type": "application/json",          Authorization: `Bearer ${process.env.NOTRA_API_KEY}`,        },        body: JSON.stringify(updates),      });      if (!response.ok) throw new Error("Failed to update post");      return response.json();    },    onSuccess: (_data, { postId }) => {      queryClient.invalidateQueries({ queryKey: ["notra", "posts"] });      queryClient.invalidateQueries({ queryKey: ["notra", "post", postId] });    },  });}

React and Next.js notes

If you use React Server Components or Next.js, React's cache() function can help dedupe repeated reads during a single render on the server.

Use it for read paths, but do not rely on it alone for mutation invalidation. Pair it with route-level revalidation, cache tags, or your client cache invalidation strategy after updates, deletes, or completed generation jobs.

Practical defaults

  • Use a short TTL for list caches if content changes frequently.
  • Use explicit invalidation after update, delete, and successful generation completion.
  • Prefer sort=desc plus page-1 refetch if your UI is a newest-first feed.
  • Cache individual posts separately from paginated lists.

Ça a marché sur votre configuration ?

Pas encore évalué