Skip to main content
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.