O que Todo Dev Deve Saber sobre Next.js App Router Avançado: Server Actions, Cache e Revalidação Já leu

Server Actions: Fundamentos e Implementação Server Actions são funções executadas no servidor que podem ser chamadas diretamente do cliente sem necessidade de criar rotas API explícitas. Elas representam um paradigma diferente de comunicação cliente-servidor: em vez de fazer requisições HTTP para endpoints, você executa código do servidor de forma declarativa e type-safe usando TypeScript. A verdadeira potência das Server Actions está na sua simplicidade e segurança. Quando você marca uma função com , o Next.js automaticamente cria um endpoint seguro para essa função, valida os tipos de dados, serializa argumentos e respostas, e previne exposição de código sensível. Você não precisa gerenciar autenticação, validação de requisição ou tratamento de CORS — tudo é abstraído pela framework. Criando sua primeira Server Action Para criar uma Server Action, defina uma função assíncrona com a diretiva no topo do arquivo. Esta função pode ser criada em um arquivo separado ou no próprio componente: Agora você pode chamar essa função diretamente de um componente

Server Actions: Fundamentos e Implementação

Server Actions são funções executadas no servidor que podem ser chamadas diretamente do cliente sem necessidade de criar rotas API explícitas. Elas representam um paradigma diferente de comunicação cliente-servidor: em vez de fazer requisições HTTP para endpoints, você executa código do servidor de forma declarativa e type-safe usando TypeScript.

A verdadeira potência das Server Actions está na sua simplicidade e segurança. Quando você marca uma função com 'use server', o Next.js automaticamente cria um endpoint seguro para essa função, valida os tipos de dados, serializa argumentos e respostas, e previne exposição de código sensível. Você não precisa gerenciar autenticação, validação de requisição ou tratamento de CORS — tudo é abstraído pela framework.

Criando sua primeira Server Action

Para criar uma Server Action, defina uma função assíncrona com a diretiva 'use server' no topo do arquivo. Esta função pode ser criada em um arquivo separado ou no próprio componente:

// app/actions/user.ts
'use server';

import { db } from '@/lib/db';

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  // Validação básica
  if (!name || !email) {
    throw new Error('Nome e email são obrigatórios');
  }

  // Executar no servidor
  const user = await db.user.create({
    data: { name, email }
  });

  return { success: true, userId: user.id };
}

Agora você pode chamar essa função diretamente de um componente cliente:

// app/components/UserForm.tsx
'use client';

import { createUser } from '@/app/actions/user';

export default function UserForm() {
  async function handleSubmit(formData: FormData) {
    try {
      const result = await createUser(formData);
      console.log('Usuário criado:', result);
    } catch (error) {
      console.error('Erro:', error);
    }
  }

  return (
    <form action={handleSubmit}>
      <input type="text" name="name" placeholder="Nome" required />
      <input type="email" name="email" placeholder="Email" required />
      <button type="submit">Criar Usuário</button>
    </form>
  );
}

Trabalhando com useTransition para melhor UX

Quando você chama uma Server Action, o estado é mantido em aberto até que a requisição seja concluída. Use o hook useTransition para fornecer feedback ao usuário durante a execução:

'use client';

import { useTransition } from 'react';
import { createUser } from '@/app/actions/user';

export default function UserForm() {
  const [isPending, startTransition] = useTransition();

  function handleSubmit(formData: FormData) {
    startTransition(async () => {
      try {
        const result = await createUser(formData);
        alert(`Usuário ${result.userId} criado com sucesso!`);
      } catch (error) {
        alert('Erro ao criar usuário');
      }
    });
  }

  return (
    <form action={handleSubmit}>
      <input type="text" name="name" placeholder="Nome" disabled={isPending} />
      <input type="email" name="email" placeholder="Email" disabled={isPending} />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Criando...' : 'Criar Usuário'}
      </button>
    </form>
  );
}

O isPending indica quando a requisição está em andamento, permitindo desabilitar inputs, mostrar spinners ou alterar o texto do botão. Esta é a forma recomendada de gerenciar estados de carregamento em Server Actions.

Cache no Next.js App Router

O caching no Next.js é um dos aspectos mais sofisticados e frequentemente mal compreendidos. Existem quatro camadas distintas de cache que funcionam juntas: Request Memoization, Data Cache, Full Route Cache e Client-side Router Cache. Cada uma tem seu próprio tempo de vida e propósito.

O Request Memoization deduplica requisições idênticas durante uma única renderização do servidor. Se você faz duas chamadas ao banco de dados com os mesmos parâmetros na mesma requisição HTTP, apenas uma executa. O Data Cache persiste dados entre requisições no build time até que você explicitamente revalide. O Full Route Cache cacheia a renderização HTML completa. O Client-side Router Cache mantém rotas visitadas em memória no navegador.

Request Memoization: Evitando duplicação

React estende fetch para automaticamente memoizar requisições idênticas. Isso significa que se você chamar a mesma URL com os mesmos parâmetros múltiplas vezes durante uma renderização, apenas uma requisição de rede é feita:

// app/lib/api.ts
export async function getUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`, {
    next: { revalidate: 3600 } // 1 hora
  });
  return res.json();
}

// app/components/UserProfile.tsx
import { getUser } from '@/app/lib/api';

export default async function UserProfile({ userId }: { userId: string }) {
  // Ambas as chamadas resultam em apenas uma requisição
  const user = await getUser(userId);
  const userAgain = await getUser(userId);

  return <div>{user.name}</div>;
}

Este mecanismo é transparente. Na prática, significa que você pode estruturar código sem se preocupar com requisições duplicadas na mesma renderização.

Data Cache e revalidação com next/cache

O Data Cache persiste dados entre requisições até revalidação. Por padrão, fetch() com next.revalidate cria dados cacheados. Você pode controlar o cache de forma granular:

// app/lib/database.ts
import { cache } from 'react';

// Esta função é cacheada por requisição e revalidada a cada 1 hora
export const getProduct = cache(async (id: string) => {
  const res = await fetch(`https://api.shop.com/products/${id}`, {
    next: { revalidate: 3600 }
  });
  return res.json();
});

// app/products/[id]/page.tsx
import { getProduct } from '@/app/lib/database';

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span>${product.price}</span>
    </div>
  );
}

// Exports estáticos para otimizar o build
export async function generateStaticParams() {
  const products = await fetch('https://api.shop.com/products').then(r => r.json());
  return products.map((p) => ({ id: p.id }));
}

Full Route Cache e Incremental Static Regeneration (ISR)

O Full Route Cache combina renderização estática com revalidação agendada. Rotas estáticas são pré-renderizadas no build time e servidas do cache até a revalidação:

// app/blog/[slug]/page.tsx
import { getPost } from '@/lib/posts';

export const revalidate = 60; // Revalidar a cada 60 segundos

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <time>{post.publishedAt}</time>
    </article>
  );
}

export async function generateStaticParams() {
  // Gerar parâmetros para posts populares no build
  const posts = await fetch('https://api.blog.com/posts?featured=true').then(r => r.json());
  return posts.map((post) => ({ slug: post.slug }));
}

Quando uma requisição chega em /blog/meu-artigo, se o cache ainda é válido, a resposta cacheada é retornada instantaneamente. Após 60 segundos, a próxima requisição dispara uma revalidação em segundo plano.

Revalidação: Atualizando dados em tempo real

Revalidação é o mecanismo que permite atualizar dados cacheados sem reconstruir toda a aplicação. Existem duas estratégias: time-based revalidation (revalidação em intervalos) e on-demand revalidation (revalidação acionada por eventos).

A revalidação time-based é declarativa e automática — você define um intervalo e o Next.js cuida do resto. A revalidação on-demand é imperativa — você explicitamente informa ao Next.js que dados específicos mudaram e precisam ser atualizados.

On-demand revalidation com revalidatePath e revalidateTag

revalidatePath() invalida o cache de uma rota específica. Use-a quando um usuário executa uma ação que afeta aquela página:

// app/actions/blog.ts
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function publishPost(postId: string) {
  // Atualizar no banco de dados
  const post = await db.post.update({
    where: { id: postId },
    data: { published: true }
  });

  // Revalidar a página do blog
  revalidatePath('/blog');
  revalidatePath(`/blog/${post.slug}`);

  return post;
}

revalidateTag() oferece controle mais granular. Você marca requisições com tags e depois invalida especificamente essas tags:

// app/lib/posts.ts
export async function getPost(slug: string) {
  const res = await fetch(`https://api.blog.com/posts/${slug}`, {
    next: { tags: ['post', `post-${slug}`] }
  });
  return res.json();
}

// app/actions/blog.ts
'use server';

import { revalidateTag } from 'next/cache';

export async function updatePost(slug: string, data: any) {
  const res = await fetch(`https://api.blog.com/posts/${slug}`, {
    method: 'PUT',
    body: JSON.stringify(data)
  });

  // Revalidar apenas posts com esta tag
  revalidateTag(`post-${slug}`);
  revalidateTag('post'); // Ou a tag genérica

  return res.json();
}

Esta abordagem é superior para cenários com múltiplos recursos, pois permite invalidar especificamente o que mudou sem tocar em outros caches.

Revalidação em Server Actions com feedback imediato

Combine Server Actions com revalidação para criar experiências reativas onde os dados são atualizados imediatamente após uma ação:

// app/actions/comments.ts
'use server';

import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';

export async function addComment(postId: string, content: string) {
  // Validar e inserir
  if (!content.trim()) {
    throw new Error('Comentário não pode estar vazio');
  }

  const comment = await db.comment.create({
    data: { postId, content }
  });

  // Revalidar o cache de comentários para este post
  revalidateTag(`comments-${postId}`);

  return comment;
}

// app/lib/comments.ts
export async function getComments(postId: string) {
  const res = await fetch(`https://api.blog.com/comments/${postId}`, {
    next: { tags: [`comments-${postId}`] }
  });
  return res.json();
}

// app/components/CommentSection.tsx
'use client';

import { addComment } from '@/app/actions/comments';
import { useTransition } from 'react';

export default function CommentSection({ postId }: { postId: string }) {
  const [isPending, startTransition] = useTransition();

  async function handleSubmit(formData: FormData) {
    const content = formData.get('content') as string;

    startTransition(async () => {
      try {
        await addComment(postId, content);
        // Após revalidação, a página será atualizada automaticamente
      } catch (error) {
        console.error('Erro ao adicionar comentário:', error);
      }
    });
  }

  return (
    <form action={handleSubmit}>
      <textarea name="content" placeholder="Seu comentário..." disabled={isPending} />
      <button disabled={isPending}>{isPending ? 'Enviando...' : 'Comentar'}</button>
    </form>
  );
}

Quando o usuário clica em "Comentar", a Server Action executa no servidor, valida os dados, insere no banco e revalida o tag de comentários. Ao retornar, o Router Cache do cliente é atualizado automaticamente e os dados frescos são exibidos.

Padrões avançados e armadilhas comuns

Dominar Server Actions, Cache e Revalidação exige compreender padrões sofisticados e evitar armadilhas que degradam performance ou criam comportamentos inesperados.

Erro: Confundir camadas de cache

Um erro comum é assumir que revalidar com revalidatePath() invalida tudo. Cada camada de cache é independente. Se você não marca uma requisição com next.revalidate ou tags, ela não é cacheada pelo Data Cache — apenas pelo Request Memoization durante aquela renderização específica:

// ❌ INCORRETO: Esta função NÃO é cacheada entre requisições
export async function getLatestPosts() {
  const res = await fetch('https://api.blog.com/posts');
  return res.json();
}

// ✅ CORRETO: Especificar cache explicitamente
export async function getLatestPosts() {
  const res = await fetch('https://api.blog.com/posts', {
    next: { revalidate: 300 } // 5 minutos
  });
  return res.json();
}

Sincronizando múltiplas revalidações

Quando uma ação afeta vários recursos, revalidate tudo simultaneamente para evitar inconsistências:

'use server';

import { revalidateTag, revalidatePath } from 'next/cache';

export async function deletePost(postId: string) {
  await db.post.delete({ where: { id: postId } });

  // Revalidar em paralelo
  await Promise.all([
    revalidateTag('posts-list'),
    revalidateTag(`post-${postId}`),
    revalidatePath('/posts'),
    revalidatePath('/dashboard')
  ]);
}

Limitando revalidação a rotas específicas com generateStaticParams

Para aplicações com milhares de rotas dinâmicas, renderizar todas no build é inviável. Use generateStaticParams para renderizar apenas as importantes e deixar outras sob demanda:

// app/products/[id]/page.tsx
export const dynamicParams = true; // Permitir parâmetros não-pré-renderizados

export async function generateStaticParams() {
  // Apenas renderizar os 100 produtos mais vendidos no build
  const products = await fetch(
    'https://api.shop.com/products?sort=sales&limit=100'
  ).then(r => r.json());

  return products.map(p => ({ id: p.id }));
}

export const revalidate = 3600; // Revalidar a cada hora

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  return <div>{product.name}</div>;
}

Outros produtos são renderizados sob demanda na primeira requisição e então cacheados.

Conclusão

Dominando Server Actions, você elimina a necessidade de criar rotas API manualmente — a framework cuida de serialização, autenticação e validação. Isso não apenas reduz código, como torna seu aplicativo mais seguro por padrão. Use useTransition para fornecer feedback durante operações assíncronas.

As quatro camadas de cache (Request Memoization, Data Cache, Full Route Cache, Client Router Cache) trabalham em conjunto para oferecer performance excepcional. Compreender qual camada você está utilizando é fundamental. Request Memoization é transparente. Data Cache requer declaração explícita com next.revalidate ou tags. Full Route Cache funciona apenas com rotas renderizáveis estaticamente.

A revalidação on-demand com revalidateTag() é mais poderosa que revalidatePath() em aplicações complexas porque oferece controle granular sobre quais dados foram afetados. Combine Server Actions com revalidação para criar experiências reativas onde dados são atualizados imediatamente após uma ação do usuário.

Referências


Artigos relacionados