DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Designing CDN Caching with Claude Code: Cache-Control, Surrogate Keys, Tag-based Purging

Introduction

Delegating API response caching to CDN can reduce origin server traffic by 90%. But misconfigured Cache-Control headers deliver stale data to users. Generate designs with Claude Code.


CLAUDE.md CDN Cache Rules

## CDN Cache Design Rules

### Cache-Control Header Strategy
- Public, immutable content: public, max-age=31536000, immutable
- Public, updatable: public, max-age=60, stale-while-revalidate=300
- Authenticated users: private, no-store (don't cache at CDN)
- API responses (no auth required): public, max-age=30, s-maxage=300

### Purge Strategy
- Tag-based purge with Surrogate-Key (Fastly) / Cache-Tag (Cloudflare)
- Bulk purge all related caches on entity update
- Purge-all only for emergencies

### Variations
- Include Accept-Encoding in Vary (gzip/brotli support)
- Vary: Cookie when content differs for authenticated users
- Separate multilingual content with Accept-Language
Enter fullscreen mode Exit fullscreen mode

Generated CDN Cache Implementation

// src/middleware/cacheControl.ts

type CacheStrategy = 'public-static' | 'public-short' | 'public-medium' | 'public-long' | 'private' | 'no-cache';

const CACHE_HEADERS: Record<CacheStrategy, string> = {
  'public-static': 'public, max-age=31536000, immutable',
  'public-short': 'public, max-age=30, s-maxage=300, stale-while-revalidate=60',
  'public-medium': 'public, max-age=300, s-maxage=1800, stale-while-revalidate=600',
  'public-long': 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=3600',
  'private': 'private, no-store',
  'no-cache': 'no-cache, no-store, must-revalidate',
};

export function setCacheControl(strategy: CacheStrategy) {
  return (_req: Request, res: Response, next: NextFunction) => {
    res.setHeader('Cache-Control', CACHE_HEADERS[strategy]);
    next();
  };
}

export function addSurrogateKeys(keys: string[]) {
  return (_req: Request, res: Response, next: NextFunction) => {
    res.setHeader('Surrogate-Key', keys.join(' ')); // Fastly
    res.setHeader('Cache-Tag', keys.join(','));     // Cloudflare
    next();
  };
}
Enter fullscreen mode Exit fullscreen mode
// Product list: public + 5-min cache + tag-based purge
router.get('/products', setCacheControl('public-medium'), addSurrogateKeys(['products', 'product-list']), async (req, res) => {
  const products = await prisma.product.findMany({ where: { published: true }, orderBy: { createdAt: 'desc' } });
  res.json(products);
});

// Authenticated user profile: don't cache at CDN
router.get('/me', authenticate, setCacheControl('private'), async (req, res) => {
  const user = await prisma.user.findUniqueOrThrow({ where: { id: req.user.id }, select: { id: true, name: true, email: true } });
  res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

Tag-Based Cache Purging

// Fastly purge by Surrogate-Key
async function purgeFastlyByTag(tag: string): Promise<void> {
  const response = await fetch(
    `https://api.fastly.com/service/${process.env.FASTLY_SERVICE_ID}/purge/${tag}`,
    { method: 'POST', headers: { 'Fastly-Key': process.env.FASTLY_API_KEY!, 'Accept': 'application/json' } }
  );
  if (!response.ok) throw new Error(`Fastly purge failed: ${response.status}`);
  logger.info({ tag }, 'Fastly cache purged');
}

// Auto-purge on product update
export async function invalidateProductCache(productId: string): Promise<void> {
  await Promise.allSettled([
    purgeFastlyByTag(`product:${productId}`),
    purgeFastlyByTag('product-list'),
  ]);
}

// Prisma middleware: auto-purge after DB writes
prisma.$use(async (params, next) => {
  const result = await next(params);
  if (['create', 'update', 'delete'].includes(params.action) && params.model === 'Product') {
    const productId = params.args.where?.id ?? result?.id;
    if (productId) {
      setImmediate(() => invalidateProductCache(productId).catch(err => logger.error({ err }, 'Cache invalidation failed')));
    }
  }
  return result;
});
Enter fullscreen mode Exit fullscreen mode

Summary

Design CDN Caching with Claude Code:

  1. CLAUDE.md — document Cache-Control strategy by content type, purge policy
  2. s-maxage — separate CDN TTL from browser TTL (long CDN, short browser)
  3. Surrogate-Key / Cache-Tag — attach entity ID tags for pinpoint purging
  4. Prisma middleware — automatically purge cache on DB updates

Review CDN cache designs with **Code Review Pack (¥980)* using /code-review at prompt-works.jp*

myouga (@myougatheaxo) — Axolotl VTuber.

Top comments (0)