O desenvolvimento web está em constante pendulação. Houve uma época em que tudo era renderizado no servidor (PHP, Ruby on Rails). Depois, movemos tudo para o cliente (SPAs com React, Vue). Agora, com o advento dos React Server Components (RSC) e do Next.js App Router, estamos encontrando um meio-termo poderoso: o melhor dos dois mundos.
E a jóia da coroa dessa nova era no ecossistema React são as Server Actions.
Com o lançamento do Next.js 15, as Server Actions deixaram de ser experimentais e se tornaram o padrão recomendado para lidar com mutações de dados. Neste guia massivo, vamos explorar cada detalhe dessa feature, desde o básico até padrões avançados de segurança e UX. Se você ainda está escrevendo pages/api/submit.ts para processar formulários, prepare-se para aposentar muito código boilerplate.
O Que São Server Actions, Afinal?
Em termos simples, Server Actions são funções assíncronas que são executadas no servidor, mas que podem ser invocadas diretamente de componentes do cliente ou do servidor.
Antes, para enviar um formulário, o fluxo era:
- Criar um componente de formulário no cliente (
"use client"). - Criar um estado
useStatepara os inputs. - Criar uma função
onSubmitque faz umfetch('/api/submit', ...). - Criar um arquivo de API Route (
pages/api/submit.ts) para receber o request, validar e salvar no banco. - Tratar erros, loading states e revalidação de dados manualmente.
Com Server Actions, o fluxo é:
- Criar uma função
asyncque salva no banco. - Passar essa função para a prop
actiondo<form>.
Fim. O Next.js cuida da comunicação, serialização e execução. É a Remote Procedure Call (RPC) feita da maneira certa para a web.
Configuração Inicial no Next.js 15
No Next.js 15, as Server Actions já vêm habilitadas por padrão. Você não precisa alterar o next.config.js.
A convenção é simples:
- Para definir ações que podem ser importadas em componentes Client, crie um arquivo com a diretiva
"use server"no topo. - Para ações inline em componentes Server, adicione
"use server"dentro da função.
Exemplo Básico: O "Hello World" das Actions
Vamos criar uma ação para salvar um post de blog. Crie um arquivo src/app/actions.ts:
// src/app/actions.ts 'use server' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export async function createPost(formData: FormData) { const title = formData.get('title') as string const content = formData.get('content') as string // Validação básica (em produção use Zod!) if (!title || !content) { throw new Error('Campos obrigatórios faltando') } // Interação direta com o banco (sem API Routes!) await db.post.create({ data: { title, content } }) // A mágica: atualiza a UI instantaneamente revalidatePath('/blog') }
E no seu componente:
// src/components/create-post-form.tsx import { createPost } from '@/app/actions' export function CreatePostForm() { return ( <form action={createPost} className="p-4 border rounded"> <input name="title" placeholder="Título" className="border p-2 mb-2 w-full" /> <textarea name="content" placeholder="Conteúdo" className="border p-2 w-full" /> <button type="submit" className="bg-blue-500 text-white p-2 rounded"> Salvar Post </button> </form> ) }
Note a ausência de JavaScript no cliente para o envio. Esse formulário funciona até mesmo se o usuário desabilitar o JS no navegador (Progressive Enhancement), embora em aplicações modernas raramente dependamos disso.
Gerenciamento de Estado de Carregamento (useFormStatus)
Uma UX ruim é clicar em "Salvar" e não ter feedback. Como estamos usando a prop action nativa do HTML, não temos um useState(loading) manual. O React nos fornece o hook useFormStatus para isso.
Nota: useFormStatus deve ser usado em um componente renderizado dentro do <form>.
// src/components/submit-button.tsx 'use client' import { useFormStatus } from 'react-dom' export function SubmitButton() { const { pending } = useFormStatus() return ( <button type="submit" disabled={pending} className="bg-blue-500 disabled:bg-gray-400 text-white p-2 rounded flex items-center gap-2" > {pending ? 'Salvando...' : 'Salvar Post'} {pending && <Spinner />} </button> ) }
Agora basta usar <SubmitButton /> dentro do formulário do exemplo anterior.
Tratamento de Erros e Feedback (useActionState)
E se o banco de dados falhar? Ou a validação? Simplesmente lançar um erro (throw new Error) não é a melhor UX. Queremos retornar mensagens de erro para o formulário.
Para isso, usamos o hook useActionState (anteriormente useFormState no React experimental). Ele permite que a Server Action retorne um valor que é atualizado no cliente.
Refatorando nossa action:
// src/app/actions.ts 'use server' export type FormState = { message: string; errors?: { title?: string[]; content?: string[]; }; } export async function createPostSafely(prevState: FormState, formData: FormData): Promise<FormState> { // Simulação de validação com Zod const validatedFields = schema.safeParse({ title: formData.get('title'), content: formData.get('content'), }); if (!validatedFields.success) { return { message: 'Erro de validação', errors: validatedFields.error.flatten().fieldErrors } } try { await db.post.create({ data: validatedFields.data }) } catch (e) { return { message: 'Erro ao salvar no banco de dados' } } revalidatePath('/blog') return { message: 'Post criado com sucesso!' } }
E no componente cliente:
'use client' import { useActionState } from 'react' import { createPostSafely } from '@/app/actions' const initialState = { message: '', errors: {} } export function AdvancedForm() { const [state, formAction] = useActionState(createPostSafely, initialState) return ( <form action={formAction}> {state.message && <p className="text-red-500">{state.message}</p>} <input name="title" /> {state.errors?.title && <p className="text-red-500 text-sm">{state.errors.title[0]}</p>} {/* ... restante do form ... */} </form> ) }
Revalidação de Cache: O Poder do revalidatePath
No modelo antigo, após uma mutação, você precisava refazer o fetch dos dados manualmente ou usar bibliotecas como React Query ou SWR para invalidar caches.
No Next.js, isso é integrado. A função revalidatePath(path) limpa o cache daquela rota específica. Na próxima visita (ou imediatamente, se for uma navegação SPA), o Next.js busca os dados frescos do servidor.
Também existe o revalidateTag(tag), que oferece controle granular se você estiver usando o fetch com tags (fetch(url, { next: { tags: ['posts'] } })).
Atualizações Otimistas (useOptimistic)
Para aplicações que parecem "instantâneas", não queremos esperar o servidor responder para atualizar a UI. Se eu adiciono um item na lista, quero vê-lo lá agora.
O hook useOptimistic permite isso de forma elegante.
'use client' import { useOptimistic } from 'react' export function PostList({ posts }: { posts: Post[] }) { const [optimisticPosts, addOptimisticPost] = useOptimistic( posts, (state, newPost: Post) => [newPost, ...state] ); async function action(formData: FormData) { const title = formData.get('title') as string; // Atualiza a UI imediatamente addOptimisticPost({ id: Math.random(), title, content: '...' }); // Chama a Server Action real await createPost(formData); } return ( <div> <form action={action}>...</form> <ul> {optimisticPosts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ) }
Se a Server Action falhar, o React automaticamente reverte o estado otimista para o estado real anterior. É robusto e mágico.
Segurança: O Elefante na Sala
Muitos desenvolvedores olham para "use server" e pensam: "Espera, eu estou expondo meu banco de dados para o cliente?".
Não. O código da Server Action nunca é enviado para o navegador. Apenas a "assinatura" da função (uma URL de referência interna do Next.js) é exposta. No entanto, é crucial tratar Server Actions como pontos de entrada públicos da sua API.
Checklist de Segurança Obrigatório:
-
Autenticação: Sempre verifique quem está chamando a ação. Não assuma que o usuário está logado só porque o botão "Salvar" estava visível.
import { auth } from '@/auth' // Auth.js ou Clerk export async function deletePost(id: string) { const session = await auth() if (!session?.user) throw new Error('Não autorizado') // ... } -
Autorização: O usuário está logado, mas ele pode deletar esse post?
const post = await db.post.findUnique({ where: { id } }) if (post.authorId !== session.user.id) throw new Error('Proibido') -
Validação de Input: Nunca confie no
FormDataou argumentos passados. Use Zod para garantir que os dados estão no formato correto.
Server Actions vs API Routes
Afinal, as API Routes morreram?
Não extamente, mas seu uso diminuiu drasticamente.
Use Server Actions quando:
- Você está lidando com mutações de formulários.
- Você quer chamar uma função do servidor a partir de um evento no cliente (
onClick). - Você quer tipagem TypeScript end-to-end sem gerar SDKs.
Use API Routes (Route Handlers) quando:
- Você precisa expor uma API REST pública para terceiros (webhooks, mobile apps).
- Você precisa de funcionalidades específicas do protocolo HTTP que abstrações do React escondem (ex: streaming de binários customizados, headers complexos).
Conclusão
O Next.js 15 solidifica as Server Actions como uma das mudanças mais produtivas no desenvolvimento web recente. Elas eliminam a camada intermediária de "glue code" (API routes, fetchers, state managers) e permitem que você foque no que importa: lógica de negócio e interface do usuário.
Ao combinar Server Actions com useActionState, useOptimistic e validação com Zod, você cria aplicações robustas, seguras e com uma experiência de usuário de classe mundial, escrevendo uma fração do código que escreveria há 3 anos.
O futuro é server-side, mas a experiência é client-side. E as Server Actions são a ponte entre eles.
Você já migrou seus formulários para Server Actions? Qual foi o maior desafio? Deixe seu comentário!