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]' })
OptionDescription
tagInvalidate all entries carrying this tag
routeInvalidate 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',
    },
  })
}
DirectiveDescription
max-ageHow long the browser may cache the response (seconds)
s-maxageHow long a shared cache (CDN) may cache the response
stale-while-revalidateServe stale content while revalidating in the background
no-storeDisable 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