| name | data-fetching |
| description | Data fetching, caching, and cache invalidation patterns for Odyssey. Use when writing request functions, debugging stale data, adding cache tags, or implementing Server Actions that mutate data. |
| invocation | auto |
Data Fetching Patterns for Odyssey
The Golden Rule
All Strapi GET requests go through fetchAPI() in lib/utils.ts. All mutations go through Server Actions in lib/actions.ts using raw fetch().
Writing a New Request Function
Template
import { fetchAPI } from "@/lib/utils";
import { CACHE_TAGS } from "@/lib/cache-tags";
export async function getThingsByFilter(filterValue: string): Promise<Thing[]> {
return fetchAPI<Thing[]>("/things", {
urlParams: {
filters: { field: { $eq: filterValue } },
populate: {
relation: { fields: ["name", "slug"] },
},
sort: ["name:asc"],
},
next: { tags: [CACHE_TAGS.things], revalidate: 900 },
});
}
Checklist Before Writing
- Read the schema first:
backend/src/api/{type}/content-types/{type}/schema.json
- Check if a similar function exists:
Grep for the content type in lib/requests/
- Choose the right cache tag: Check
lib/cache-tags.ts for existing tags
- Decide on populate depth: Only populate fields you'll render. Never
populate: "*"
Cache Tag System
Tag Types
Global tags — shared across all users:
CACHE_TAGS.droplets;
CACHE_TAGS.playlists;
CACHE_TAGS.lesson;
CACHE_TAGS.tags;
CACHE_TAGS.announcements;
CACHE_TAGS.users;
CACHE_TAGS.allEnrollments;
Per-user tags — scoped to individual users:
CACHE_TAGS.enrollments(userId);
CACHE_TAGS.friendships(userId);
CACHE_TAGS.notes(userId);
CACHE_TAGS.highlights(userId);
Adding a New Cache Tag
- Add the constant to
lib/cache-tags.ts
- Use it in the request function's
next.tags
- Add
revalidateTag() call in every Server Action that mutates this data
- If per-user scoped, use the function form:
CACHE_TAGS.myTag(userId)
Invalidation Pattern
Every Server Action that mutates data MUST invalidate the cache:
"use server";
export async function updateThing(
thingId: number,
userId: number,
data: ThingInput,
) {
const parsed = thingSchema.safeParse(data);
if (!parsed.success) throw new Error(parsed.error.message);
const response = await fetch(`${STRAPI_URL}/api/things/${thingId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${STRAPI_ACCESS_TOKEN}`,
},
body: JSON.stringify({ data: parsed.data }),
});
if (!response.ok)
throw new Error(`Failed to update thing: ${response.status}`);
const json = await response.json();
const result = flattenAttributes(json.data);
revalidateTag(CACHE_TAGS.things);
return result;
}
Invalidation Matrix
| Mutation | Tags to invalidate |
|---|
| Create/update droplet | CACHE_TAGS.droplets |
| Create/update lesson | CACHE_TAGS.lesson, CACHE_TAGS.droplets |
| Enroll/complete/rate | CACHE_TAGS.enrollments(userId), CACHE_TAGS.allEnrollments |
| Add/remove friend | CACHE_TAGS.friendships(userId) |
| Create/delete note | CACHE_TAGS.notes(userId) |
| Create/delete highlight | CACHE_TAGS.highlights(userId) |
| Update user profile | CACHE_TAGS.users |
| Publish announcement | CACHE_TAGS.announcements |
The flattenAttributes Rule
| Method | Auto-flattens? | Action needed |
|---|
fetchAPI() | Yes | None — returns flat data |
Raw fetch() in Server Actions | No | Call flattenAttributes(json.data) manually |
| In tests | N/A | Mock fetchAPI with flat data |
Populate Presets
Reuse existing populate configs instead of duplicating:
import { ENROLLMENT_POPULATES } from "@/lib/requests/enrollment-populates";
import { USER_POPULATES } from "@/lib/requests/user-populates";
getEnrollmentsByAuthorizedUser(userId, {
populate: ENROLLMENT_POPULATES.dashboard,
});
Available presets:
ENROLLMENT_POPULATES.minimal — just IDs
ENROLLMENT_POPULATES.withLessonIds — includes viewedLessons
ENROLLMENT_POPULATES.dashboard — droplet name/slug/lessons for progress
ENROLLMENT_POPULATES.favorites — droplet with tags
Common Mistakes
cache + next together — Mutually exclusive in Next.js 15. Passing both silently breaks caching.
- Forgetting
flattenAttributes() — Raw fetch() returns nested Strapi format. Must flatten manually.
- Hardcoded tag strings — Always use
CACHE_TAGS constants. Hardcoded strings break grep-based debugging.
- Missing
revalidateTag() — Cache serves stale data until explicitly invalidated.
- Over-populating — Only populate relations you render. Deep populates are expensive.
- Using
getCachedX() in Server Actions — cache() only works within a single render, not in actions.
- Per-user tag without userId —
CACHE_TAGS.enrollments doesn't exist. Use CACHE_TAGS.enrollments(userId).
Per-Request Deduplication
Wrap frequently-reused fetches with React cache() in lib/requests/cached.ts:
import { cache } from "react";
export const getCachedThing = cache(async (id: number) => {
return getThingById(id);
});
Use getCachedX() in Server Components when multiple components need the same data. The cache lasts one HTTP request — not across requests.