Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,017 changes: 1,017 additions & 0 deletions API_ROUTES_REFACTORING_GUIDE.md

Large diffs are not rendered by default.

166 changes: 166 additions & 0 deletions CLAUDE_CODE_PROMPT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Claude Code Agent: API Routes Refactoring Implementation

## 🎯 **Mission**

Transform this React Router v7 application into a **true API-first architecture** by implementing systematic API routes refactoring following the comprehensive guide provided.

## πŸ“‹ **Current State**

- βœ… **Working foundation**: Framework-native cache strategy with `Vercel-CDN-Cache-Control`
- βœ… **First API route**: `/api/blog` working with proper cache headers
- βœ… **Content system**: GitHub-based content with `app/modules/content/api.ts`
- βœ… **Cache module**: `app/modules/cache.ts` with caching infrastructure

**Test current API**: `curl -I https://ocobo-git-api-routes-wabdsgn.vercel.app/api/blog`

## πŸ“– **Implementation Guide**

**READ THIS FIRST**: `API_ROUTES_REFACTORING_GUIDE.md` (1,018 lines) contains:
- Complete technical specifications
- Step-by-step implementation instructions
- Code templates for all patterns
- Testing protocols for each phase
- Cache strategy optimization
- Success criteria and validation

## πŸš€ **Your Implementation Tasks**

### **Phase 1: Complete API Routes (START HERE)**

**CRITICAL FIRST STEP**:
```bash
# Rename existing API route
mv app/routes/api.blog._index.tsx app/routes/api.posts._index.tsx
# Update internal route logic to use new naming
```

**Then implement these routes in order**:
1. **`api.posts.$slug.tsx`** - Individual blog posts
2. **`api.stories._index.tsx`** - Client stories listing
3. **`api.stories.$slug.tsx`** - Individual stories
4. **`api.search.tsx`** - Search functionality
5. **`api.tags.tsx`** - Tags listing

**Pattern**: Follow the exact template in the guide (lines 72-126) for each route.

### **Phase 2: Test All Routes**
Verify each route using the testing protocols (lines 634-639 in guide).

### **Phase 2.5: Cache Strategy Cleanup**
Simplify `app/modules/cache.ts` following lines 292-586 in the guide.

### **Phase 3: Refactor Pages**
Convert HTML pages to call API routes instead of direct backend calls.

## πŸ”§ **Key Technical Requirements**

### **Cache Strategy** (CRITICAL):
```typescript
// Use differentiated caching:
'blogListing' // 1h + 2h (for new content visibility)
'blogPost' // 7d + 14d (for performance)
'storyListing' // 1h + 2h (for new content visibility)
'story' // 14d + 30d (for performance)
'static' // 30d + 60d (for maximum efficiency)
```

### **API Response Format** (MANDATORY):
```typescript
// Success
{ data: T, isError: false, total?: number }

// Error
{ data: null, isError: true, error: string }
```

### **Route Template** (EXACT PATTERN):
```typescript
import { getApiCacheHeaders, shouldBypassCache } from '~/modules/cache';

export async function loader({ request, params }: LoaderFunctionArgs) {
// 1. Extract parameters
// 2. Fetch data using existing content API
// 3. Handle errors with proper HTTP status
// 4. Apply cache headers using getApiCacheHeaders()
// 5. Return Response with proper JSON and headers
}
```

## ⚠️ **Critical Constraints**

### **DO NOT MODIFY**:
- `app/entry.server.tsx` (cache headers working perfectly)
- `app/modules/content/api.ts` (use existing functions)
- Cache header application logic (already optimized)

### **DO FOLLOW**:
- Exact response format (see guide lines 270-283)
- Cache strategy selection (see guide lines 284-291)
- Error handling patterns (see guide lines 94-107)
- Testing protocols (see guide lines 634-665)

## πŸ§ͺ **Testing Requirements**

For each route implemented:
```bash
# Test locally
curl -I http://localhost:5173/api/posts

# Test deployed
curl -I https://ocobo-git-api-routes-wabdsgn.vercel.app/api/posts

# Verify JSON response
curl -s https://ocobo-git-api-routes-wabdsgn.vercel.app/api/posts | head -20
```

**Expected headers**:
- `cache-control: s-maxage=3600, stale-while-revalidate=7200` (listings)
- `cache-control: s-maxage=604800, stale-while-revalidate=1209600` (individual content)
- `content-type: application/json`
- `vary: Accept-Language`

## πŸ“Š **Success Criteria**

### **Phase 1 Complete When**:
- βœ… All 6 API routes return proper JSON with cache headers
- βœ… All routes handle errors with appropriate HTTP status codes
- βœ… Testing protocols pass for each route
- βœ… Cache headers match expected values by content type

### **Phase 2.5 Complete When**:
- βœ… `cache.ts` reduced from 129 β†’ ~60 lines
- βœ… `createHybridLoader` removed entirely
- βœ… Separate `getApiCacheHeaders()` vs `getHtmlCacheHeaders()` functions

### **Phase 3 Complete When**:
- βœ… All HTML pages call API routes instead of direct backend functions
- βœ… No `fetchBlogposts`, `fetchStories` imports in page components
- βœ… Consistent error handling across all consumers
- βœ… Zero regression in page functionality

## 🎯 **Key Benefits You're Achieving**

- **API-first architecture**: External consumers get same data as HTML pages
- **50%+ code reduction**: Simplified cache module and page loaders
- **Optimal performance**: 95%+ GitHub API call reduction with smart caching
- **New content visibility**: Users see new articles within 1-3 hours automatically
- **Future-ready**: Foundation for React Query, mobile apps, webhooks

## πŸ“ **Implementation Notes**

1. **Start simple**: Begin with Phase 1, test thoroughly
2. **Follow patterns**: Use the exact templates provided in the guide
3. **Test frequently**: Verify each route before moving to the next
4. **Read the guide**: All edge cases and detailed instructions are documented
5. **Incremental approach**: Complete each phase fully before proceeding

## πŸ”— **Reference**

- **Complete Guide**: `API_ROUTES_REFACTORING_GUIDE.md`
- **Working Example**: `app/routes/api.blog._index.tsx` (to be renamed)
- **Content Functions**: `app/modules/content/api.ts`
- **Cache Module**: `app/modules/cache.ts`

---

**Ready to implement? Start with renaming the existing API route and then follow Phase 1 step-by-step. The comprehensive guide has all the details you need for successful implementation.**
8 changes: 3 additions & 5 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import { ServerRouter } from 'react-router';
import * as i18n from '~/localization/i18n'; // your i18n configuration file
import i18nServer from '~/localization/i18n.server';
import {
getCacheHeaders,
getCacheStrategyForPath,
getHtmlCacheHeaders,
logCacheStrategy,
shouldBypassCache,
} from '~/modules/cache';
Expand Down Expand Up @@ -67,10 +66,9 @@ export default async function handleRequest(
const stream = createReadableStreamFromReadable(body);
responseHeaders.set('Content-Type', 'text/html');

// Apply framework-native cache headers at response level
const cacheStrategy = getCacheStrategyForPath(pathname);
// Apply simplified HTML cache headers at response level
const bypassCache = shouldBypassCache(request);
const cacheHeaders = getCacheHeaders(cacheStrategy, bypassCache);
const cacheHeaders = getHtmlCacheHeaders(bypassCache);

// Set cache headers on response (bypasses Vercel framework-level override)
for (const [key, value] of Object.entries(cacheHeaders)) {
Expand Down
111 changes: 48 additions & 63 deletions app/modules/cache.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
/**
* Cache strategy for React Router loaders
*
* - Local filesystem: No caching
* - GitHub content: Vercel Edge Cache
* - Testing: ?refresh=1 bypasses cache
* Simplified cache strategy for API-first architecture
*/

import type { LoaderFunctionArgs } from 'react-router';
import { getPrivateEnvVars } from './env.server';

/**
* Cache strategy types for different content
*/
export type CacheStrategy = 'blogPost' | 'story' | 'static';
export type CacheStrategy =
| 'blogListing'
| 'blogPost'
| 'storyListing'
| 'story'
| 'static';

/**
* Cache configuration for each strategy type
*/
// BALANCED: Aggressive cache for individual content, shorter for listings to show new content
const CACHE_CONFIG = {
blogPost: { maxAge: 3600, staleWhileRevalidate: 86400 }, // 1h + 24h
story: { maxAge: 3600, staleWhileRevalidate: 86400 }, // 1h + 24h
static: { maxAge: 86400, staleWhileRevalidate: 604800 }, // 24h + 7d
// Blog post listings: Short cache to show new articles quickly
blogListing: { maxAge: 3600, staleWhileRevalidate: 7200 }, // 1h + 2h
// Individual blog posts: Long cache since content doesn't change once published
blogPost: { maxAge: 604800, staleWhileRevalidate: 1209600 }, // 7d + 14d

// Story listings: Short cache to show new stories quickly
storyListing: { maxAge: 3600, staleWhileRevalidate: 7200 }, // 1h + 2h
// Individual stories: Long cache since content doesn't change once published
story: { maxAge: 1209600, staleWhileRevalidate: 2592000 }, // 14d + 30d

// Static content: cache for 30 days + 60 days stale = 90 days total
static: { maxAge: 2592000, staleWhileRevalidate: 5184000 }, // 30d + 60d
} as const;

/**
* Check if using GitHub as content source
*/
function isUsingGitHub(): boolean {
try {
const { readContentFrom } = getPrivateEnvVars();
Expand All @@ -35,26 +35,23 @@ function isUsingGitHub(): boolean {
}
}

/**
* Check if request should bypass cache (refresh parameter)
*/
export function shouldBypassCache(request: Request): boolean {
const url = new URL(request.url);
return url.searchParams.has('refresh');
}

/**
* Build cache control header based on strategy
*/
function buildCacheControl(strategy: CacheStrategy): string {
const { maxAge, staleWhileRevalidate } = CACHE_CONFIG[strategy];
return `s-maxage=${maxAge}, stale-while-revalidate=${staleWhileRevalidate}`;
}

/**
* Get cache headers for GitHub content or local filesystem
* Cache headers for API routes (data caching)
*/
export function getCacheHeaders(strategy: CacheStrategy, bypassCache = false) {
export function getApiCacheHeaders(
strategy: CacheStrategy,
bypassCache = false,
) {
if (bypassCache) {
return {
'Cache-Control': 'no-cache, no-store, must-revalidate',
Expand All @@ -65,61 +62,49 @@ export function getCacheHeaders(strategy: CacheStrategy, bypassCache = false) {
if (isUsingGitHub()) {
const cacheControl = buildCacheControl(strategy);
return {
// Standard header for browsers and other CDNs
'Cache-Control': cacheControl,
// Vercel-specific header (highest priority, Vercel-only)
'Vercel-CDN-Cache-Control': cacheControl,
Vary: 'Accept-Language',
};
}

// Local filesystem: no caching
return {
'Cache-Control': 'no-cache',
'Vercel-CDN-Cache-Control': 'no-cache',
};
}

/**
* Hybrid loader that returns data for meta functions with cache headers
*
* This is the main loader function used throughout the application.
* It handles both data access (for meta functions) and caching.
*
* Note: Cache headers are now applied at the HTML response level in entry.server.tsx
* Cache headers for HTML pages (rendering caching)
* Shorter cache for faster content updates
*/
export function createHybridLoader<T>(
fetcher: (args: LoaderFunctionArgs) => Promise<T>,
_strategy?: CacheStrategy, // Keep for backward compatibility but no longer used
) {
return async (args: LoaderFunctionArgs) => {
const data = await fetcher(args);
return data;
};
}

/**
* Determine cache strategy based on pathname
*/
export function getCacheStrategyForPath(pathname: string): CacheStrategy {
// Remove language prefix if present
const pathWithoutLang = pathname.replace(/^\/(en|fr)/, '') || '/';

if (pathWithoutLang.startsWith('/blog')) {
return 'blogPost';
export function getHtmlCacheHeaders(bypassCache = false) {
if (bypassCache) {
return {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Vercel-CDN-Cache-Control': 'no-cache, no-store, must-revalidate',
};
}

if (pathWithoutLang.startsWith('/clients')) {
return 'story';
if (isUsingGitHub()) {
return {
'Cache-Control': 's-maxage=300, stale-while-revalidate=3600', // 5min + 1h
'Vercel-CDN-Cache-Control': 's-maxage=300, stale-while-revalidate=3600',
Vary: 'Accept-Language',
};
}

// Default to static for homepage and other pages
return 'static';
return {
'Cache-Control': 'no-cache',
'Vercel-CDN-Cache-Control': 'no-cache',
};
}

/**
* Log cache strategy being used on server startup
*/
// Removed deprecated functions:
// - createHybridLoader: Pages now use standard loaders calling API routes
// - getCacheHeaders: Replaced by getApiCacheHeaders/getHtmlCacheHeaders
// - getCacheStrategyForPath: HTML pages use unified HTML caching

export function logCacheStrategy(): void {
const strategy = isUsingGitHub()
? 'Vercel Edge Cache'
Expand Down
Loading