Caching an API response in Next.js can mean caching data fetched by your route, caching the route’s output, or letting a browser or CDN cache the HTTP response. Those are separate layers, and confusing them can leave users with unexpectedly old data—or no caching at all.
This tutorial targets the Next.js 15 App Router caching model. It shows how to cache an upstream API fetch for a limited period, invalidate cached data after a change, and keep browser and CDN behavior explicit. Check the documentation for your installed Next.js version before applying version-sensitive settings.
Key Takeaways
- In Next.js 15,
fetchis not cached by default. Setnext.revalidateto opt into time-based caching. - A cached
fetchdoes not automatically mean the browser or CDN caches your route’s HTTP response. - Next.js 15 GET Route Handlers are not cached by default. Treat route-output caching as a separate choice.
- Time-based revalidation allows data to be refreshed; it does not promise that every request after the interval waits for fresh data.
- Use
revalidatePathor cache tags after successful mutations when time-based refresh is not timely enough.
First, identify which cache you mean
Suppose a route handler calls a supplier’s API and returns the result. There are at least three possible places to cache:
| Layer | What it stores | Typical control |
|---|---|---|
| Next.js Data Cache | The result of a server-side fetch | cache and next.revalidate options |
| Route or render output | The result produced for a route | Route Handler or route-segment caching configuration; behavior depends on Next.js version and route type |
| Browser or CDN | The HTTP response sent to the client | Response Cache-Control headers and CDN configuration |
These layers can be configured independently. For example, a route can run for every request, read an upstream result from the Next.js Data Cache, and send Cache-Control: no-store so a browser or shared HTTP cache does not retain the response.
Cache an upstream API fetch for a set time
In Next.js 15, a server-side fetch is not cached by default. To reuse the result for a period, set next.revalidate in seconds. The following App Router Route Handler caches the upstream data with a 60-second revalidation interval:
// app/api/products/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const apiKey = process.env.CATALOG_API_KEY;
if (!apiKey) {
return NextResponse.json(
{ error: 'Catalog API is not configured' },
{ status: 500 },
);
}
try {
const upstreamResponse = await fetch('https://catalog.example.com/v1/products', {
headers: { Authorization: `Bearer ${apiKey}` },
next: { revalidate: 60 },
});
if (!upstreamResponse.ok) {
return NextResponse.json(
{ error: 'Catalog service returned an error' },
{ status: 502 },
);
}
const products = await upstreamResponse.json();
return NextResponse.json(products, {
headers: { 'Cache-Control': 'private, no-store' },
});
} catch {
return NextResponse.json(
{ error: 'Catalog service is unavailable' },
{ status: 502 },
);
}
}
Replace the example hostname and response handling with your provider’s actual API. Keep credentials in server-side environment variables, not in client code. The example’s Cache-Control header is deliberately separate from next.revalidate: Next.js may reuse the upstream fetch result, while the HTTP response tells browsers and shared caches not to store this route’s response.
A revalidation interval is not a strict freshness guarantee. Once the interval has elapsed, Next.js can serve the existing cached value while revalidating it in the background. If a refresh fails, stale data may continue to be served while another attempt is made. This behavior avoids making every visitor wait for the upstream service, but it means the first response after expiry is not necessarily fresh.
Use cache: 'no-store' when every request must go to the upstream service and the request should not use the Data Cache:
const response = await fetch('https://catalog.example.com/v1/products', {
cache: 'no-store',
});
Use cache: 'force-cache' when deliberately caching the result without a time-based revalidation interval. An indefinitely cached value is often a poor fit for changing API data unless you have a dependable invalidation strategy.
Understand Route Handler behavior separately
In Next.js 15, GET Route Handlers are not cached by default. That is separate from whether a fetch inside a handler is cached. The example above can therefore execute its handler for each request while reusing the upstream result from the Data Cache.
If you want the route’s output itself to be cached, opt in using a supported mechanism for your Next.js version and route. Route segment options such as revalidate or dynamic affect route behavior; they are not interchangeable with next.revalidate on an individual fetch. Check the Route Segment Config documentation, particularly if your handler uses request-specific data such as cookies, headers, or query parameters.
Do not assume a route is cached just because its method is GET, or because a deployment platform offers a CDN. Confirm the behavior in the deployed environment and inspect the response headers and platform caching configuration.
Invalidate cached data after a change
Time-based revalidation is useful when some delay is acceptable. For an update that should trigger a refresh sooner, invalidate the relevant route after the mutation succeeds. For example, a Server Action that updates products can call revalidatePath:
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function updateProduct(productId: string, name: string) {
// Replace this with your database update. Await it so the cache is
// invalidated only after the write succeeds.
await saveProduct(productId, name);
revalidatePath('/api/products');
}
saveProduct represents your application’s database function; it is not a Next.js API. Put invalidation after the successful write so a failed update does not trigger a misleading refresh. Choose a path that corresponds to the cached route or data in your application, and verify the result in your deployment.
For data shared across multiple routes, cache tags can be more targeted than invalidating a path. Attach a tag to the fetch with next: { tags: ['products'] }, then invalidate that tag after a mutation using the revalidateTag API documented for your installed Next.js version. Tag invalidation signatures and freshness behavior are version-sensitive; follow the current API documentation rather than copying an older one-argument example without checking it.
Keep browser and CDN caching explicit
The next.revalidate option controls Next.js’s server-side Data Cache behavior. It does not set the browser’s HTTP cache lifetime. Similarly, setting Cache-Control on a response does not by itself configure the Next.js Data Cache.
Choose HTTP response headers based on what clients are allowed to store:
- Use
Cache-Control: private, no-storewhen the response is user-specific or should not be retained by browsers or shared caches. - For public, identical responses, an HTTP caching policy may be appropriate. Set it deliberately and confirm how your hosting platform’s CDN interprets it.
- Avoid shared caching for responses that vary by authorization, cookies, or other user-specific input unless the cache key and privacy implications are fully understood.
If you configure both Next.js data revalidation and an HTTP cache lifetime, document both. A CDN may continue serving its own cached response even after Next.js refreshes the underlying data, depending on the deployment and headers.
Common Mistakes
- Assuming
fetchis cached by default in Next.js 15. Opt in withnext.revalidateor another intentional cache setting. - Treating a revalidation interval as a maximum data age. Revalidation can be stale-while-revalidate; it does not guarantee that the first request after expiry receives fresh data.
- Confusing a cached fetch with a cached route. They are different layers with different controls.
- Setting a CDN header and expecting it to control Next.js’s Data Cache. HTTP response caching and server-side fetch caching are separate.
- Caching personalized data in a shared cache. Consider authorization, cookies, response variation, and cache keys before enabling shared caching.
- Invalidating before a write succeeds. Complete the mutation first, then invalidate the relevant path or tag.
- Copying cache APIs across Next.js versions without checking. Defaults and invalidation APIs change; confirm the documentation for your installed version.
Best Practices
- Start with the data’s freshness needs: choose
no-store, a measured revalidation interval, or explicit invalidation. - Configure the fetch cache and HTTP response cache independently.
- Keep sensitive credentials on the server and avoid shared caching for user-specific results.
- Handle upstream HTTP errors and network failures rather than returning a successful-looking empty result.
- Test both a cache hit and a refresh after the interval or an invalidation. Check production behavior, not only local development.
- Record the Next.js version your app uses and recheck caching documentation during upgrades.
Conclusion
For Next.js 15, next.revalidate is a practical way to cache upstream API data for a limited period, while cache: 'no-store' opts out of the Data Cache. Route output and browser/CDN caching are separate decisions. Make each layer explicit, and use path or tag invalidation when waiting for the next time-based refresh is not acceptable.
References
- Next.js
fetchAPI reference — fetch cache options and time-based revalidation. - Next.js
revalidatePathAPI reference — path-based on-demand revalidation. - Next.js
revalidateTagAPI reference — tag-based invalidation and version-specific API details. - Next.js caching guide — overview of caching behavior across the App Router.
