Busca de dados e cache no Next.js (App Router)
1. Visão geral
Next.js App Router faz cache automático dos resultados de fetch no servidor para reduzir a carga do backend e acelerar a página. Mas esse mecanismo de cache é bem diferente do cache HTTP tradicional que você conhece, e sem entender bem, você facilmente esbarra em situações de “dados antigos que não atualizam” ou ao contrário “toda página renderiza do zero, insanamente lenta”.
Este artigo passa pelos níveis de cache do Next.js: cache de fetch, revalidação por tempo, revalidação por tag e renderização dinâmica. Após ler, você saberá escolher a estratégia de fetch correta para cada tipo de página (estática, ISR, dinâmica) e conseguirá debugar quando o cache se recusar a cooperar.
Nota: este artigo aplica-se ao App Router (Next.js 13+, recomenda-se 14/15). Pages Router usa um mecanismo diferente (
getStaticProps,getServerSideProps) que está fora do escopo deste artigo.
2. Quatro níveis de cache do Next.js
Next.js tem 4 mecanismos de cache distintos, fáceis de confundir se você os misturar:
| Mecanismo | Local | Propósito |
|---|---|---|
| Request Memoization | Servidor, durante uma única renderização | Agrupa requisições fetch idênticas na mesma requisição |
| Data Cache | Servidor, entre requisições | Faz cache dos resultados de fetch a longo prazo, pode revalidar |
| Full Route Cache | Servidor, tempo de build | Faz cache de HTML + carga útil RSC de rotas estáticas |
| Router Cache | Cliente, durante a sessão | Faz cache dos resultados de navegação para retorno/avanço rápido |
Este artigo foca no Data Cache porque é isso que você controla diretamente através das opções de fetch.
3. Fetch padrão: cache “infinito”
Em um Server Component, fetch é patchado pelo Next.js para fazer cache automático por padrão:
// app/products/page.tsx
async function getProducts() {
const res = await fetch("https://api.shop.example.com/products");
if (!res.ok) throw new Error("Não consigo chamar a API de produtos");
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((p: { id: string; name: string }) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
fetchsem passarcache→ Next.js usaforce-cachepor padrão.- O resultado é armazenado em Data Cache, compartilhado com todas as requisições seguintes, não expira automaticamente a menos que você revalide.
- Esta página é adicionada ao Full Route Cache: constrói HTML estático, servido como página estática.
É por isso que muita gente fica surpresa: muda os dados no backend mas a página não muda — porque Next.js está tratando como dados estáticos.
4. Controlando cache com a opção cache
Para sempre pegar dados novos, use no-store:
async function getOrderStatus(orderId: string) {
const res = await fetch(
`https://api.shop.example.com/orders/${orderId}`,
{ cache: "no-store" }
);
return res.json();
}
cache: "no-store"desativa completamente o Data Cache para esta requisição.- A rota contendo esse fetch será marcada como dinâmica, renderizada novamente a cada requisição.
- Adequado para dados que mudam continuamente: pedidos em processamento, preços em tempo real, dashboard.
Nota: basta um
fetchusarno-storena página, toda a página muda para renderização dinâmica — mesmo que outrosfetches na mesma página ainda usemforce-cache.
5. Revalidação por tempo (ISR)
Para fazer cache mas se auto-renovar após N segundos (Incremental Static Regeneration), use next.revalidate:
async function getCategories() {
const res = await fetch(
"https://api.shop.example.com/categories",
{ next: { revalidate: 300 } } // 5 minutos
);
return res.json();
}
revalidate: 300significa: os dados são considerados “antigos” após 300 segundos.- A primeira requisição após os 300s ainda retorna dados antigos (stale-while-revalidate) mas Next.js ativa um novo fetch em segundo plano.
- A próxima requisição recebe os dados novos.
Pode ser aplicado a toda a rota exportando uma variável no início do arquivo:
// app/blog/[slug]/page.tsx
export const revalidate = 300;
export default async function BlogPost({ params }: { params: { slug: string } }) {
const res = await fetch(`https://api.blog.example.com/posts/${params.slug}`);
return <article>{/* ... */}</article>;
}
export const revalidateaplica-se a todofetchna rota se o fetch não declararrevalidatepróprio.- Se um
fetchespecífico declarar umnext.revalidatediferente, aquele valor vence para esse fetch.
6. Revalidação por tag: revalidateTag
Revalidação baseada em tempo é boa para conteúdo que muda regularmente, mas não se adequa quando você quer atualizar imediatamente quando os dados mudam (por exemplo, admin acaba de atualizar o preço do produto). Neste caso, use tags de cache.
// app/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(
`https://api.shop.example.com/products/${id}`,
{ next: { tags: [`product-${id}`] } }
);
return res.json();
}
Quando o admin atualiza um produto, chame revalidateTag em um Route Handler ou Server Action:
// app/actions/update-product.ts
"use server";
import { revalidateTag } from "next/cache";
export async function updateProduct(id: string, formData: FormData) {
await fetch(`https://api.shop.example.com/products/${id}`, {
method: "PATCH",
body: formData,
});
revalidateTag(`product-${id}`); // apaga cache com essa tag
}
tagsadiciona um “rótulo” à entrada de cache do fetch.revalidateTag(tag)deleta todas as entradas de Data Cache com aquela tag, não importa qual rota esteja usando.- O próximo acesso buscará dados novos e fará cache com a tag novamente.
Este é o mecanismo mais poderoso porque separa a lógica “quando invalidar” de “qual página exibe os dados” — semelhante ao cache tagging em Spring Cache ou Redis.
7. Revalidação por caminho: revalidatePath
Se não quiser usar tags, você pode revalidar diretamente pelo caminho da rota:
"use server";
import { revalidatePath } from "next/cache";
export async function publishPost(slug: string) {
// ... chama API de publicação ...
revalidatePath(`/blog/${slug}`); // apenas esta página
revalidatePath("/blog", "page"); // ou a lista inteira
}
revalidatePath("/blog/[slug]")deleta o Full Route Cache + Data Cache daquela rota específica.- O segundo parâmetro (
"page"ou"layout") decide se também revalida rotas filhas que usam aquele layout. - Diferente de
revalidateTag:revalidatePathestá ligada a uma rota específica, enquanto tag pode ser usada em várias rotas diferentes.
Nota:
revalidateTag/revalidatePathsó funcionam em Server Actions ou Route Handlers, não podem ser chamadas diretamente de um Client Component.
8. Renderização dinâmica: quando o cache não faz sentido
Algumas APIs fazem Next.js automaticamente mudar uma rota para dinâmica, ignorando o Full Route Cache:
import { cookies, headers } from "next/headers";
export default async function AccountPage() {
const sessionToken = (await cookies()).get("session")?.value;
const userAgent = (await headers()).get("user-agent");
const res = await fetch("https://api.shop.example.com/me", {
headers: { Authorization: `Bearer ${sessionToken}` },
cache: "no-store",
});
return <div>Olá, {(await res.json()).name}</div>;
}
- Chamar
cookies()ouheaders()marca automaticamente a rota como dinâmica — faz sentido porque os dados dependem do usuário específico. - Combine com
cache: "no-store"para garantir que os dados sejam sempre atualizados de acordo com a sessão.
Force dinâmica de forma explícita (por exemplo, quando você sabe com certeza que não pode fazer cache):
export const dynamic = "force-dynamic";
9. Route Handlers também têm cache próprio
Route Handlers (app/api/.../route.ts) com método GET também seguem as mesmas regras de cache de um Server Component:
// app/api/stock/route.ts
export const revalidate = 60;
export async function GET() {
const res = await fetch("https://api.shop.example.com/stock");
const data = await res.json();
return Response.json(data);
}
- Se
GETnão usar o objetoRequest(não lêrequest.urlpara pegar query) então pode ser cache estático. - Se o handler usar
request.headers,request.cookies, ou método diferente deGET(POST,PUT…) → sempre dinâmico, sem cache.
10. Client Component: cache pertence a outra biblioteca
O cache de fetch do Next.js só se aplica no servidor. Em um Client Component, você faz fetch por conta própria usando useEffect ou uma biblioteca de cache como SWR:
npm install swr
"use client";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function CartBadge({ userId }: { userId: string }) {
const { data, isLoading } = useSWR(`/api/cart/${userId}`, fetcher, {
refreshInterval: 10_000, // faz poll a cada 10s
});
if (isLoading) return <span>...</span>;
return <span>{data.itemCount} itens</span>;
}
- SWR tem cache, deduplicação de requisições, revalidação em foco — completamente independente do Data Cache do servidor.
- Use quando precisar de dados atualizados continuamente no cliente sem recarregar a página inteira.
11. Escrever testes para lógica relacionada a chaves de cache
O próprio mecanismo de cache do Next.js é difícil de testar em unidade diretamente, mas a lógica de gerar tags/paths geralmente vale a pena testar separadamente — evita erros de digitação que fazem revalidateTag não coincidir.
// lib/cache-keys.ts
export function productTag(id: string): string {
return `product-${id}`;
}
export function categoryTag(id: string): string {
return `category-${id}`;
}
// lib/cache-keys.test.ts
import { describe, it, expect } from "vitest";
import { productTag, categoryTag } from "./cache-keys";
describe("cache-keys", () => {
it("gera tag de produto no formato correto", () => {
expect(productTag("sku-123")).toBe("product-sku-123");
});
it("tags de produto e categoria não se sobrepõem", () => {
const id = "42";
expect(productTag(id)).not.toBe(categoryTag(id));
});
});
- Separar funções que geram tags em um módulo próprio ajuda a garantir que o código que chama
fetch(adiciona tag) e o que chamarevalidateTag(remove tag) sempre usem o mesmo formato. - Este é um erro real muito comum: um lado escreve
product-${id}, o outro escreveproducts-${id}→ revalidate nunca coincide.
✓ cache-keys > gera tag de produto no formato correto
✓ cache-keys > tags de produto e categoria não se sobrepõem
12. Erros comuns
- Esquecer que fetch faz cache infinito por padrão: após deploy, os dados não mudam porque ainda está usando cache antigo do build anterior (com static export) ou do primeiro fetch (com server deploy). Corrija com
revalidateouno-store. - Misturar
no-storee cache no mesmo componente desnecessariamente: basta 1 fetch comno-storee toda a página fica dinâmica, perdendo todo o benefício de cache dos outros fetches. - Chamar
revalidateTag/revalidatePathfora de Server Actions/Route Handlers: não tem efeito, Next.js vai dar erro ou silenciosamente ignorar dependendo da versão. - Usar
fetchnormalmente (não patchado pelo Next.js) dentro de bibliotecas de terceiros (por exemplo, SDK usandoaxiosouundicipuro): não é gerenciado pelo Data Cache, você precisa revalidar de outra forma (por exemplo, cache em outra camada ou escrever um wrapper chamandofetchpadrão). - Não tagear por entidade específica: usar tags muito genéricas (
"products"para todos os produtos) causa uma pequena atualização invalidar todo o cache, perdendo o benefício de cache por item.
Quando NÃO fazer cache: dados personalizados por sessão (carrinho, saldo, notificações), dados em tempo real (preços de ações, status de pagamento), ou qualquer resposta que dependa de
cookies()/headers(). Esses casos devem usarno-storeouforce-dynamicdesde o início, não tente forçar cache depois revalidar continuamente — é complicado e não é a natureza certa dos dados.
13. Conclusão
O cache no Next.js App Router tem muitas camadas, mas a parte que você controla ativamente é principalmente o Data Cache através das opções de fetch: cache, next.revalidate, next.tags. Regra de escolha rápida: dados que mudam raramente → revalidate por tempo; dados que mudam por evento específico (admin atualiza) → adicione tags e use revalidateTag; dados por usuário ou em tempo real → no-store/force-dynamic. Dominando esses três grupos, você raramente terá que ficar se perguntando “por que os dados não atualizam”.