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
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();
};
}
// 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);
});
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;
});
Summary
Design CDN Caching with Claude Code:
- CLAUDE.md — document Cache-Control strategy by content type, purge policy
- s-maxage — separate CDN TTL from browser TTL (long CDN, short browser)
- Surrogate-Key / Cache-Tag — attach entity ID tags for pinpoint purging
- 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)