09-nextjsTermsLevel_07Caching Route Handlers

Caching Route Handlers

Level 7 — API & Route Handlers The specific rules and behaviors that determine when a Next.js route.ts API endpoint caches its response at build-time versus calculating it dynamically on every request.


1. Prerequisites


2. Term Category

Server & Edge API (Route Handler Caching Behavior): Route Handlers are cached statically by default when using GET methods without dynamic request inspection.


3. Explanation

Environment Context

  • Server Only (Build-Time & Request-Time)

(1) Design Motivation — "Why did we design this?"

If you build an API endpoint GET /api/products that just returns a static JSON list of products from a file, it is extremely inefficient for the server to read that file and generate the JSON 10,000 times for 10,000 visitors. Next.js optimizes Route Handlers by aggressively evaluating them at Build Time. If it determines the endpoint is static, it caches the JSON response forever. When a user requests the endpoint, Next.js just serves the cached string instantly. Understanding how Next.js decides if a route is Static (cached) or Dynamic (not cached) is critical.

(2) The Rules of Static vs Dynamic

By default, GET requests are cached (Static).

However, Next.js will automatically switch your GET route to Dynamic (re-evaluated on every request) if it detects you doing any of the following:

  1. Using the Request object (e.g., reading headers or URL search params).
  2. Using dynamic functions like cookies() or headers().
  3. Using dynamic folder segments (e.g., app/api/users/[id]/route.ts).
  4. Using an HTTP method other than GET (e.g., POST, PUT, DELETE are NEVER cached).

(3) Forcing Dynamic Behavior

If your GET route doesn't use the Request object or cookies, but it checks a database that changes frequently, Next.js will incorrectly cache it forever! You must manually force it to be dynamic using Route Segment Config.

// app/api/time/route.ts

// This forces Next.js to run this route dynamically on every request!
export const dynamic = 'force-dynamic'; 

export async function GET() {
  // If we didn't export `force-dynamic`, this time would be permanently
  // frozen at whatever time the `npm run build` command was executed!
  return Response.json({ time: new Date().toISOString() });
}

4. Common Mistakes & Pitfalls

Mistake 1: Stale API Responses in Production

The mistake: A developer writes a GET Route Handler that queries a database to get a list of active users. They test it in development (npm run dev) and it updates perfectly. They deploy it to production, and the API always returns the exact same list of users forever.

Why it's wrong: In Development mode, caching is largely disabled so you can see your changes. But during the Production Build (npm run build), Next.js looks at the route, sees no Request usage, and caches the database result statically. Golden Rule: If a GET Route Handler returns data that can change independently of a code deployment (like database queries), you MUST add export const dynamic = 'force-dynamic'; or export const revalidate = 0; to the file!


Mistake 2: Assuming POST, PUT, or DELETE Route Handlers Are Cached Automatically

The mistake: Expecting a POST /api/orders Route Handler response to be cached in Next.js Data Cache.

Why it's wrong: Next.js caches ONLY GET Route Handlers. Non-GET HTTP methods (POST, PUT, DELETE) are ALWAYS evaluated dynamically on every request.

Incorrect:

/* Expecting POST route handler responses to be cached */

Fix:

/* GET route handlers are cached by default; POST/PUT/DELETE are always dynamic */

Mistake 3: Forgetting export const dynamic = 'force-dynamic' on GET Handlers Reading DB State

The mistake: Creating a GET Route Handler app/api/users/route.ts executing database queries without disabling static caching.

Why it's wrong: By default, GET Route Handlers returning static responses are evaluated and cached at BUILD TIME. New database entries added in production will NOT appear unless force-dynamic is set.

Incorrect:

// app/api/users/route.ts
export async function GET() {
  const users = await db.user.findMany(); // ❌ Evaluated once at BUILD time!
  return Response.json(users);
}

Fix:

// app/api/users/route.ts
export const dynamic = 'force-dynamic'; // Enforce dynamic request-time evaluation
export async function GET() {
  const users = await db.user.findMany();
  return Response.json(users);
}

5. Practice Exercises

Exercise 1: Configuring Static Route Handler Caching

Scenario: Create a static Route Handler app/api/static-data/route.ts that caches JSON responses at build time.

Requirements:

  1. Export GET handler returning Response.json().
Answer

Implementation

// app/api/static-data/route.ts
export async function GET() {
  const data = { version: "1.0.0", buildTime: new Date().toISOString() };
  return Response.json(data);
}

Technical Explanation

  1. In Next.js App Router, GET Route Handlers without dynamic parameters are cached statically by default.
  2. Executed once during next build and served as static JSON artifacts.
  3. Delivers ultra-fast CDN edge response performance.

Exercise 2: Opting Out of Route Handler Caching with dynamic = 'force-dynamic'

Scenario: Force a GET Route Handler to bypass static caching and run dynamically on every request.

Requirements:

  1. Export export const dynamic = 'force-dynamic'.
Answer

Implementation

// app/api/live-status/route.ts
export const dynamic = "force-dynamic";

export async function GET() {
  return Response.json({
    status: "online",
    timestamp: Date.now()
  });
}

Technical Explanation

  1. export const dynamic = 'force-dynamic' instructs Next.js to bypass static route caching.
  2. Re-evaluates the Route Handler logic on Node.js/edge servers for every incoming HTTP request.
  3. Essential for real-time telemetry or status endpoints.

Exercise 3: Time-Based Route Handler Revalidation (revalidate)

Scenario: Configure a GET Route Handler to revalidate cached JSON output every 60 seconds.

Requirements:

  1. Export export const revalidate = 60.
Answer

Implementation

// app/api/news/route.ts
export const revalidate = 60;

export async function GET() {
  const news = await fetch("https://api.example.com/raw-news").then((r) => r.json());
  return Response.json(news);
}

Technical Explanation

  1. export const revalidate = seconds enables Stale-While-Revalidate (SWR) for the Route Handler.
  2. Caches JSON responses for 60 seconds before triggering background revalidation.
  3. Efficient API response caching pattern.


7. Key Takeaways

  • GET Route Handlers are cached statically by default.
  • POST, PUT, and DELETE handlers are never cached.
  • A GET route becomes dynamic automatically if it reads the Request object, reads cookies(), or uses [dynamic] folder names.
  • You can manually force a route to never cache by exporting export const dynamic = 'force-dynamic';.
  • Development mode behaves differently than Production mode regarding caching. Always verify your API caching strategy!
Built with LogoFlowershow