Caching
Catmint provides built-in caching primitives for route-level response caching, build-time pre-rendering, and programmatic cache invalidation. This guide covers cachedRoute, staticRoute, invalidateCache, and CDN integration via cache headers.
Route-Level Caching with cachedRoute
Use cachedRoute() from catmint/cache to cache the response of a route handler for a specified duration. The first argument is the handler function, and the second is an optional CacheOptions object:
// app/api/posts/endpoint.ts
import { cachedRoute } from 'catmint/cache'
import { db } from '../../lib/db.server'
// Cache the response for 60 seconds
export const GET = cachedRoute(async (request) => {
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 20,
})
return Response.json(posts)
}, { revalidate: 60 })
When a cached response exists and has not expired, the handler is skipped entirely and the cached response is returned. After the TTL elapses, the next request triggers the handler again and refreshes the cache.
Caching with Dynamic Keys
Cache keys are generated from the handler name and its serialized arguments. Handlers called with different arguments produce different cache entries:
// app/api/posts/[slug]/endpoint.ts
import { cachedRoute } from 'catmint/cache'
import { db } from '../../../lib/db.server'
// Each unique set of arguments gets its own cache entry
export const GET = cachedRoute(async (request, { params }) => {
const post = await db.post.findUnique({
where: { slug: params.slug },
})
if (!post) {
return new Response('Not found', { status: 404 })
}
return Response.json(post)
}, { revalidate: 300 })
Build-Time Pre-Rendering with staticRoute
Use staticRoute() to pre-render a route at build time. The handler runs once during catmint build and the result is saved as a static file:
// app/api/config/endpoint.ts
import { staticRoute } from 'catmint/cache'
export const GET = staticRoute(async () => {
return Response.json({
version: '1.0.0',
features: ['rsc', 'streaming', 'middleware'],
buildTime: new Date().toISOString(),
})
})
Static routes are ideal for content that does not change between deployments: configuration endpoints, sitemap generation, RSS feeds, or marketing pages with infrequent updates.
Static routes cannot access runtime request data (headers, cookies, query parameters) because they are rendered at build time, not at request time.
Cache Invalidation with invalidateCache
Use invalidateCache() from catmint/cache to programmatically clear cached responses. Pass either a tag or a route to target specific entries:
// app/api/posts/endpoint.ts
import { invalidateCache } from 'catmint/cache'
import { db } from '../../lib/db.server'
export async function POST(request: Request) {
const body = await request.json()
const post = await db.post.create({
data: body,
})
// Invalidate all entries tagged 'posts'
await invalidateCache({ tag: 'posts' })
return Response.json(post, { status: 201 })
}
You can also invalidate by route pattern:
// Invalidate all cached entries for a specific route
await invalidateCache({ route: '/blog/[slug]' })
| Option | Description |
|---|---|
tag | Invalidate all entries carrying this tag |
route | Invalidate all entries associated with this route pattern |
Cache Headers for CDN Integration
For deployments behind a CDN (Cloudflare, Vercel, Fastly, etc.), set standard cache headers on responses to leverage edge caching:
// app/api/products/endpoint.ts
import { db } from '../../lib/db.server'
export async function GET() {
const products = await db.product.findMany()
return new Response(JSON.stringify(products), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=60, s-maxage=300, stale-while-revalidate=600',
},
})
}
| Directive | Description |
|---|---|
max-age | How long the browser may cache the response (seconds) |
s-maxage | How long a shared cache (CDN) may cache the response |
stale-while-revalidate | Serve stale content while revalidating in the background |
no-store | Disable caching entirely (for sensitive data) |
Combining cachedRoute with CDN Headers
You can use cachedRoute for server-side caching and set Cache-Control headers for CDN-level caching. They operate independently:
// app/api/feed/endpoint.ts
import { cachedRoute } from 'catmint/cache'
import { db } from '../../lib/db.server'
export const GET = cachedRoute(async () => {
const items = await db.feedItem.findMany({ take: 50 })
return new Response(JSON.stringify(items), {
headers: {
'Content-Type': 'application/json',
// Server cache: 120s (from cachedRoute)
// CDN cache: 300s
// Browser cache: 60s
'Cache-Control': 'public, max-age=60, s-maxage=300',
},
})
}, { revalidate: 120 })
Next Steps
- API Endpoints -- build HTTP handlers that you can cache
- Server Functions -- server-side logic with createServerFn
- Middleware -- add cache headers globally via middleware
