Tutorial de Server Actions no Next.js: boas práticas para formulários e validação

Você está diante do computador, olhando o código de um formulário de cadastro. A pasta já tem quatro arquivos: componente do formulário, API Route, definições de tipos, tratamento de erros… Para processar o envio de um formulário simples, o código já está chegando a 200 linhas.
Será que existe um jeito mais simples?
A resposta são as Server Actions. Esse recurso do App Router do Next.js pode simplificar em 80% o processamento de formulários. Você não precisa criar uma API Route, chamar fetch manualmente nem manter toda aquela lógica trabalhosa de estado. Parece ótimo, mas algumas dúvidas são inevitáveis: isso é realmente seguro? Como fazer a validação? E como controlar o estado de carregamento?
Eu também tive essas dúvidas quando comecei a usar o recurso. Depois de alguns meses, alguns tropeços e bastante aprendizado, reuni práticas úteis para trabalhar com formulários usando Server Actions no Next.js. Vamos do envio mais básico à validação com Zod, segurança e experiência do usuário, sempre com exemplos reais de código.
Fundamentos das Server Actions
O que são Server Actions?
Server Actions são funções assíncronas executadas no servidor. Você as marca com 'use server' e pode usá-las diretamente no atributo action de um formulário. Quando o formulário é enviado, a função é chamada automaticamente; processamento de dados, operações no banco e atualização de cache acontecem no servidor.
Suas principais características são:
- Segurança de tipos: o TypeScript consegue verificar todo o fluxo
- Configuração zero: não é preciso criar uma pasta
/api - Processamento automático: o FormData é passado automaticamente
Há duas formas de escrever uma Server Action: diretamente no componente, de forma inline, ou em um arquivo separado, no nível do módulo:
// Opção 1: inline no componente
export default function Page() {
async function createUser(formData: FormData) {
'use server' // Marca a função como uma Server Action
const name = formData.get('name')
// Processa os dados...
}
return <form action={createUser}>...</form>
}
// Opção 2: arquivo separado (recomendado)
// app/actions.ts
'use server' // Marca o arquivo inteiro
export async function createUser(formData: FormData) {
const name = formData.get('name')
// Processa os dados...
}
Talvez você esteja se perguntando: qual é a diferença entre Server Actions e as tradicionais API Routes? Quando usar cada uma?
Preparei uma tabela comparativa:
| Característica | Server Actions | API Routes |
|---|---|---|
| Uso | Envio de formulários e alterações de dados | API RESTful e chamadas externas |
| Métodos HTTP | Apenas POST | GET, POST, PUT, DELETE etc. |
| Segurança de tipos | Nativa | Exige definição manual de tipos |
| Forma de chamada | Chamada direta da função | Requisição com fetch |
| Cenário ideal | Lógica interna e formulários | API pública e integrações de terceiros |
| Quantidade de código | Menor | Relativamente maior |
Em resumo: use Server Actions internamente e API Routes externamente. Se você só precisa processar formulários do próprio aplicativo, Server Actions são suficientes. Se precisa oferecer uma interface para outros sistemas ou aceitar requisições GET, continue usando API Routes.
Segundo uma pesquisa da Vercel de 2025, 63% dos desenvolvedores já usam Server Actions em produção. Portanto, o recurso deixou de ser algo experimental.
"63% dos desenvolvedores já usam Server Actions em produção"
Primeiro exemplo com Server Actions
Vamos direto ao código com um formulário de login simples:
// app/login/page.tsx
export default function LoginPage() {
async function handleLogin(formData: FormData) {
'use server' // Marca a função para execução no servidor
// Obtém os dados do formulário
const email = formData.get('email') as string
const password = formData.get('password') as string
// Processa o login (exemplo simplificado)
console.log('Tentativa de login:', email)
// Em um projeto real, verificaria o usuário e geraria um token
}
return (
<form action={handleLogin}>
<input
type="email"
name="email"
placeholder="E-mail"
required
/>
<input
type="password"
name="password"
placeholder="Senha"
required
/>
<button type="submit">Entrar</button>
</form>
)
}
É só isso. Os pontos principais são:
'use server': informa ao Next.js que a função deve ser executada no servidorformData.get(): obtém o valor usando o atributonamedo campoaction={handleLogin}: chama a função automaticamente quando o formulário é enviado
Ao clicar no botão, o navegador não recarrega a página; os dados são enviados diretamente ao servidor para processamento. Isso elimina uma pilha de código com fetch, useState e tratamento de erros.
Mas esse é apenas o caso mais básico. Em um projeto real, ainda é preciso validar os dados, exibir erros e controlar o estado de carregamento. É o que veremos agora.
Validação de formulários na prática
Usando Zod para validar o formulário
Confiar apenas no atributo required do cliente? Isso não é suficiente. Qualquer pessoa pode abrir as ferramentas de desenvolvimento do navegador e contornar essa validação. A validação no servidor é obrigatória.
É aí que entra o Zod. Ele valida o formato dos dados no servidor e retorna um erro assim que encontra um problema, evitando que dados inválidos cheguem ao banco.
Primeiro, instale o Zod:
npm install zod
Depois, defina as regras de validação:
// app/actions.ts
'use server'
import { z } from 'zod'
// Define o schema de validação
const SignupSchema = z.object({
name: z.string().min(2, 'O nome deve ter pelo menos 2 caracteres'),
email: z.string().email('Formato de e-mail inválido'),
password: z.string().min(8, 'A senha deve ter pelo menos 8 caracteres'),
})
export async function signup(formData: FormData) {
// Extrai os dados do FormData
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
}
// Valida os dados
const result = SignupSchema.safeParse(rawData)
// Retorna os erros se a validação falhar
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors, // Erros por campo
}
}
// Processa a lógica de negócio depois da validação
const { name, email, password } = result.data
// Cria o usuário, salva no banco etc.
console.log('Criando usuário:', { name, email })
return {
success: true,
message: 'Cadastro realizado com sucesso!',
}
}
Pontos importantes:
safeParsenão lança exceções: quando a validação falha, retorna{ success: false, error: ... }, permitindo tratar o erro de forma controladaflatten().fieldErrors: converte os erros para um formato como{ name: ['erro1'], email: ['erro2'] }, fácil de exibir- Retorno estruturado: inclui o indicador
successe as mensagens de erro para que o cliente decida o que mostrar
Mas ainda falta uma coisa: como exibir esses erros no formulário? Para isso, vamos usar useActionState.
Exibindo erros de validação com useActionState
useActionState é um Hook introduzido no React 19, anteriormente chamado useFormState, criado especificamente para lidar com o estado retornado por Server Actions. Ele:
- Armazena no estado do componente os dados retornados pelo servidor
- Fornece uma função action encapsulada
- Informa se o formulário está sendo enviado
Veja o código:
// app/signup/page.tsx
'use client' // O uso de Hooks exige um componente cliente
import { useActionState } from 'react'
import { signup } from '@/app/actions'
export default function SignupPage() {
// Define o estado inicial
const initialState = { success: false, errors: {}, message: '' }
// useActionState recebe a Server Action e o estado inicial
const [state, formAction, isPending] = useActionState(signup, initialState)
return (
<form action={formAction}> {/* Usa formAction no lugar da action original */}
<div>
<label>Nome</label>
<input
type="text"
name="name"
required
/>
{/* Exibe o erro do campo */}
{state.errors?.name && (
<p className="error">{state.errors.name[0]}</p>
)}
</div>
<div>
<label>E-mail</label>
<input
type="email"
name="email"
required
/>
{state.errors?.email && (
<p className="error">{state.errors.email[0]}</p>
)}
</div>
<div>
<label>Senha</label>
<input
type="password"
name="password"
required
/>
{state.errors?.password && (
<p className="error">{state.errors.password[0]}</p>
)}
</div>
<button type="submit" disabled={isPending}>
{isPending ? 'Enviando...' : 'Cadastrar'}
</button>
{/* Exibe a mensagem de sucesso */}
{state.success && (
<p className="success">{state.message}</p>
)}
</form>
)
}
O fluxo funciona assim:
- O usuário envia o formulário →
signupé chamada - A validação falha no servidor → retorna
{ success: false, errors: {...} } useActionStatearmazena o resultado emstate- O componente renderiza novamente e exibe as mensagens de erro
O valor isPending é true enquanto o formulário está sendo enviado e volta para false quando o processo termina. Você pode usá-lo para desabilitar o botão e exibir um texto de carregamento.
Talvez você tenha percebido outro problema: se a validação falhar, o conteúdo preenchido pelo usuário será perdido. Para preservá-lo, você pode incluir um campo values no retorno e usar defaultValue nos inputs. Não entraremos nesse detalhe agora; o importante é entender a função do useActionState: conectar o componente cliente às Server Actions e simplificar o controle de estado.
Melhorando a experiência do usuário
Estado de carregamento e prevenção de envios repetidos
Acima usamos isPending para exibir o carregamento, mas existe outro Hook: useFormStatus. É fácil confundir os dois; eu também tive essa dúvida no começo.
Em resumo:
isPendingdouseActionState: ideal para uso dentro do componente do formuláriopendingdouseFormStatus: ideal para um componente filho do formulário, como o botão de envio
useFormStatus tem uma restrição: precisa ser chamado em um componente filho do <form>, não diretamente no componente que declara o formulário. Pode parecer inconveniente, mas permite extrair um botão reutilizável.
Veja como separar o botão de envio:
// components/SubmitButton.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton({ children }: { children: React.ReactNode }) {
const { pending } = useFormStatus() // Obtém o estado de envio do formulário
return (
<button
type="submit"
disabled={pending}
className={pending ? 'loading' : ''}
>
{pending ? 'Enviando...' : children}
</button>
)
}
Depois, use-o diretamente no formulário:
// app/signup/page.tsx
'use client'
import { useActionState } from 'react'
import { signup } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'
export default function SignupPage() {
const [state, formAction] = useActionState(signup, { success: false, errors: {} })
return (
<form action={formAction}>
{/* Campos do formulário... */}
<SubmitButton>Cadastrar</SubmitButton> {/* Controla o carregamento automaticamente */}
{state.errors?.general && (
<p className="error">{state.errors.general}</p>
)}
</form>
)
}
Assim, toda a lógica de carregamento fica encapsulada no botão. Durante o envio:
- O botão é desabilitado automaticamente, impedindo envios repetidos
- O texto muda para “Enviando…”
- Você pode adicionar uma animação de carregamento
Qual é a diferença entre pending e isPending?
| Característica | isPending do useActionState | pending do useFormStatus |
|---|---|---|
| Onde chamar | Dentro do componente do formulário | Dentro de um componente filho do formulário |
| Uso ideal | Quando é preciso acessar o estado geral do formulário | Botão independente que só precisa saber se há um envio em andamento |
| Flexibilidade | Fornece state e pending ao mesmo tempo | Fornece apenas pending |
Em projetos reais, costumo seguir esta regra:
- Formulário com lógica complexa e vários estados →
useActionState - Botão de envio genérico →
useFormStatus
Aprimoramento progressivo
Server Actions oferecem um recurso interessante: aprimoramento progressivo. Isso significa que o formulário continua funcionando mesmo quando o JavaScript está desabilitado no navegador do usuário.
Isso acontece porque, por baixo dos panos, Server Actions continuam usando o mecanismo nativo de envio do <form>. Quando há JavaScript, o Next.js intercepta o envio e o transforma em uma requisição AJAX. Sem JavaScript, o formulário volta ao comportamento tradicional.
Na prática, esse cenário não é tão comum. Hoje, poucos sites continuam úteis sem JavaScript. Ainda assim, é uma vantagem para acessibilidade e rastreadores, e você não precisa fazer nada: o Next.js cuida disso automaticamente.
Segurança e boas práticas
Segurança das Server Actions
Esta é a parte mais fácil de ignorar. Muita gente acredita que, por serem executadas no servidor, Server Actions são automaticamente seguras. Isso está completamente errado.
Uma Server Action é, essencialmente, um endpoint público de API. O Next.js gera um ID difícil de adivinhar, mas isso é apenas uma forma de ocultação, não uma medida de segurança real. Alguém com conhecimentos técnicos pode abrir as ferramentas de desenvolvimento, observar as requisições de rede, encontrar o ID da Action e chamá-la manualmente.
O Next.js oferece algumas proteções nativas:
- Proteção contra CSRF: Server Actions só aceitam requisições POST e verificam se os cabeçalhos Origin e Host correspondem. Requisições entre sites são recusadas.
- ID seguro para a Action: cada Action recebe um ID criptografado, difícil de enumerar.
- Criptografia de variáveis do closure: se a Action usar variáveis externas, o Next.js as criptografa.
Mas isso está longe de ser suficiente. Você precisa implementar estas medidas:
1. Validação de entrada
Nunca confie nos dados do cliente. A validação com Zod apresentada anteriormente é obrigatória.
2. Autenticação
Verifique se o usuário está conectado. Toda Action que exige acesso restrito precisa autenticar a identidade.
3. Autorização
Estar autenticado não significa ter permissão. O usuário A não pode excluir os dados do usuário B, por exemplo; é preciso verificar a autorização para cada operação.
Veja um exemplo prático:
// app/actions.ts
'use server'
import { cookies } from 'next/headers'
import { z } from 'zod'
const DeletePostSchema = z.object({
postId: z.string().min(1),
})
export async function deletePost(formData: FormData) {
// 1. Valida a entrada
const rawData = {
postId: formData.get('postId'),
}
const result = DeletePostSchema.safeParse(rawData)
if (!result.success) {
return { success: false, error: 'Requisição inválida' }
}
const { postId } = result.data
// 2. Autenticação: verifica se o usuário está conectado
const cookieStore = await cookies()
const sessionToken = cookieStore.get('session')?.value
if (!sessionToken) {
return { success: false, error: 'Faça login primeiro' }
}
// 3. Obtém o usuário atual
const currentUser = await getUserFromSession(sessionToken)
if (!currentUser) {
return { success: false, error: 'A sessão expirou' }
}
// 4. Autorização: verifica se a publicação pertence ao usuário atual
const post = await getPost(postId)
if (!post) {
return { success: false, error: 'Publicação não encontrada' }
}
if (post.authorId !== currentUser.id) {
return { success: false, error: 'Você não tem permissão para excluir esta publicação' }
}
// 5. Executa a operação
await deletePostFromDB(postId)
return { success: true, message: 'Exclusão concluída' }
}
Esse exemplo demonstra o fluxo completo de segurança: validação da entrada → autenticação → autorização → execução da operação. Nenhuma etapa pode ser omitida.
Também vale conhecer a biblioteca next-safe-action. Ela oferece um mecanismo de middleware para centralizar validação, autenticação e tratamento de erros:
import { createSafeActionClient } from 'next-safe-action'
// Cria um cliente de actions com autenticação
const actionClient = createSafeActionClient({
// Middleware: verifica se o usuário está conectado
async middleware() {
const session = await getSession()
if (!session) {
throw new Error('Usuário não autenticado')
}
return { userId: session.userId }
},
})
// A verificação de autenticação é aplicada automaticamente
export const deletePost = actionClient
.schema(DeletePostSchema)
.action(async ({ parsedInput, ctx }) => {
const { postId } = parsedInput
const { userId } = ctx // Obtém o ID do usuário por meio do middleware
// Executa a exclusão...
})
Assim, todas as Actions que exigem autenticação reutilizam a mesma lógica, deixando o código muito mais limpo.
Lembre-se: Server Actions não são mágica; elas são endpoints de API. Nenhuma medida de segurança necessária pode ser ignorada.
Exemplo prático: formulário com autenticação
Vamos montar um exemplo completo: um formulário de comentários que só pode ser enviado por usuários autenticados.
// app/actions.ts
'use server'
import { cookies } from 'next/headers'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
const CommentSchema = z.object({
postId: z.string(),
content: z.string().min(1, 'O comentário não pode ficar vazio').max(500, 'O comentário pode ter no máximo 500 caracteres'),
})
export async function addComment(formData: FormData) {
// 1. Valida a entrada
const rawData = {
postId: formData.get('postId'),
content: formData.get('content'),
}
const result = CommentSchema.safeParse(rawData)
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
}
}
// 2. Autenticação
const cookieStore = await cookies()
const sessionToken = cookieStore.get('session')?.value
if (!sessionToken) {
return {
success: false,
error: 'Faça login antes de comentar',
}
}
const user = await getUserFromSession(sessionToken)
if (!user) {
return {
success: false,
error: 'A sessão expirou. Faça login novamente',
}
}
// 3. Salva o comentário
const { postId, content } = result.data
await saveComment({
postId,
content,
authorId: user.id,
authorName: user.name,
createdAt: new Date(),
})
// 4. Revalida o cache da página para exibir o comentário imediatamente
revalidatePath(`/posts/${postId}`)
return {
success: true,
message: 'Comentário publicado com sucesso',
}
}
Componente cliente:
// app/posts/[id]/CommentForm.tsx
'use client'
import { useActionState } from 'react'
import { addComment } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'
export function CommentForm({ postId }: { postId: string }) {
const [state, formAction] = useActionState(addComment, {
success: false,
errors: {},
})
return (
<form action={formAction}>
{/* Campo oculto para passar postId */}
<input type="hidden" name="postId" value={postId} />
<textarea
name="content"
placeholder="Escreva seu comentário..."
rows={4}
required
/>
{state.errors?.content && (
<p className="error">{state.errors.content[0]}</p>
)}
{state.error && (
<p className="error">{state.error}</p>
)}
{state.success && (
<p className="success">{state.message}</p>
)}
<SubmitButton>Publicar comentário</SubmitButton>
</form>
)
}
Esse exemplo reúne todos os pontos apresentados até aqui:
- Validação da entrada com Zod
- Verificação da sessão do usuário
- Controle de estado com
useActionState - Atualização do cache com
revalidatePath - Botão de envio com estado de carregamento
É um fluxo completo de processamento de formulários, pronto para produção.
Técnicas avançadas
Passando parâmetros adicionais
Às vezes, é preciso enviar parâmetros que não fazem parte dos campos do formulário. Ao editar uma publicação, por exemplo, além do conteúdo você precisa passar o ID da publicação.
Uma opção é usar um campo oculto:
<input type="hidden" name="postId" value={postId} />
Mas existe uma solução mais elegante: o método bind do JavaScript.
// app/actions.ts
'use server'
export async function updatePost(postId: string, formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// Atualiza a publicação...
await updatePostInDB(postId, { title, content })
return { success: true }
}
No cliente:
// app/posts/[id]/edit/page.tsx
'use client'
import { updatePost } from '@/app/actions'
export default function EditPost({ postId }: { postId: string }) {
// Usa bind para fixar o parâmetro postId
const updatePostWithId = updatePost.bind(null, postId)
return (
<form action={updatePostWithId}>
<input type="text" name="title" required />
<textarea name="content" required />
<button type="submit">Atualizar</button>
</form>
)
}
bind(null, postId) cria uma nova função com postId fixado como primeiro parâmetro. Quando o formulário é enviado, o FormData entra como segundo parâmetro.
Essa abordagem é útil em operações de edição, exclusão e outras ações que precisam receber um ID.
Revalidação de dados
Depois que uma Server Action altera os dados, o cache das páginas relacionadas pode ficar desatualizado. O Next.js oferece duas funções para atualizá-lo:
1. revalidatePath
Atualiza por caminho:
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
// Cria a publicação...
// Atualiza a lista de publicações na página inicial
revalidatePath('/')
// Atualiza a página de detalhes da publicação
revalidatePath(`/posts/${newPostId}`)
return { success: true }
}
2. revalidateTag
Atualiza por tag, que precisa ter sido definida previamente no fetch:
// Define a tag ao buscar os dados
fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// Atualiza o cache de todos os dados com a tag 'posts' na Server Action
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
// Cria a publicação...
revalidateTag('posts') // Atualiza todos os caches relacionados
return { success: true }
}
Quando usar cada uma?
- Poucos caminhos fixos →
revalidatePath - Dados distribuídos por várias páginas →
revalidateTag
Normalmente prefiro revalidatePath, por ser simples e direto. Só uso tags quando uma operação afeta muitas páginas.
Atualizações otimistas
Algumas operações quase nunca falham, como curtir ou favoritar uma publicação. Nesses casos, você pode usar uma atualização otimista: mostrar o sucesso primeiro na interface e concluir o envio em segundo plano.
O React 19 oferece o Hook useOptimistic:
'use client'
import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [optimisticLikes, setOptimisticLikes] = useOptimistic(initialLikes)
async function handleLike() {
// Atualiza a interface imediatamente de forma otimista
setOptimisticLikes(optimisticLikes + 1)
// Envia em segundo plano
await likePost(postId)
}
return (
<button onClick={handleLike}>
👍 {optimisticLikes}
</button>
)
}
Quando o usuário clica, o número aumenta imediatamente, sem esperar a resposta do servidor. A interação fica muito mais fluida.
Mas tenha cuidado: use essa técnica apenas em operações com uma taxa de sucesso muito alta. Se a operação falhar, será preciso reverter a interface, o que pode complicar o código.
Conclusão
Depois de tudo isso, vale guardar três ideias:
-
Server Actions simplificam o processamento de formulários, mas não resolvem tudo. Use-as em formulários internos e mantenha Route Handlers para APIs externas. Não tente encaixar todas as situações em Server Actions.
-
A segurança é sua responsabilidade. O framework oferece apenas proteções básicas; validação de entrada, autenticação e autorização continuam sendo indispensáveis. Não espere que o Next.js faça tudo sozinho.
-
Os detalhes da experiência do usuário importam. Estado de carregamento, mensagens de erro, atualizações otimistas… São esses detalhes que fazem o aplicativo parecer apenas aceitável ou realmente agradável. Combine
useActionStateeuseFormStatuspara cuidar bem deles.
Comece pelo formulário mais simples. Crie uma Server Action, adicione validação com Zod e mostre um estado de carregamento; com isso, você já domina 80% do uso. Os 20% restantes, como atualização de cache e atualizações otimistas, podem ser consultados quando surgirem no projeto.
Next.js e React evoluem rapidamente, e a API das Server Actions ainda pode mudar. Acompanhe a documentação oficial para não deixar os exemplos deste artigo envelhecerem depressa demais.
Agora experimente no seu projeto. Na próxima vez que precisar processar um formulário, talvez você descubra que a solução pode ser muito mais simples.
Fluxo completo para processar formulários com Server Actions
Etapas completas para criar uma Server Action, adicionar validação e controlar o estado.
⏱️ Estimated time: 30 min
- 1
Step 1: Criar uma Server Action
Crie uma Server Action no arquivo app/actions.ts:
1. Marque o arquivo: adicione 'use server' no início
2. Defina a função: export async function actionName(formData: FormData)
3. Obtenha os dados: use formData.get('fieldName') para ler os campos do formulário
4. Retorne o resultado no formato { success: boolean, errors?: {}, message?: string }
Exemplo:
```typescript
'use server'
export async function signup(formData: FormData) {
const name = formData.get('name') as string
// Lógica de processamento...
return { success: true, message: 'Cadastro realizado com sucesso' }
}
``` - 2
Step 2: Adicionar validação com Zod
Use Zod para validar os dados no servidor:
1. Instale o Zod: npm install zod
2. Defina o schema: const SignupSchema = z.object({ name: z.string().min(2), email: z.string().email() })
3. Valide os dados: const result = SignupSchema.safeParse(rawData)
4. Trate os erros: if (!result.success) return { success: false, errors: result.error.flatten().fieldErrors }
Pontos principais:
• safeParse não lança uma exceção; ele retorna { success, data/error }
• flatten().fieldErrors converte os erros para o formato { field: ['error1'] }
• Em caso de falha, retorne erros estruturados para que o cliente possa exibi-los - 3
Step 3: Controlar o estado com useActionState
Use useActionState em um componente cliente:
1. Importe o Hook: import { useActionState } from 'react'
2. Defina o estado inicial: const initialState = { success: false, errors: {} }
3. Use o Hook: const [state, formAction, isPending] = useActionState(action, initialState)
4. Vincule o formulário: <form action={formAction}>
5. Exiba o erro: {state.errors?.field && <p>{state.errors.field[0]}</p>}
6. Exiba o carregamento: <button disabled={isPending}>{isPending ? 'Enviando...' : 'Enviar'}</button>
Fluxo:
• O usuário envia → a action é chamada → o resultado retorna → o state é atualizado → o componente renderiza novamente - 4
Step 4: Adicionar autenticação e verificação de permissões
Adicione verificações de segurança à Server Action:
1. Validação de entrada: valide todos os dados com Zod
2. Autenticação: verifique o token da sessão
```typescript
const cookieStore = await cookies()
const sessionToken = cookieStore.get('session')?.value
if (!sessionToken) return { success: false, error: 'Faça login primeiro' }
```
3. Autorização: verifique a permissão para a operação
```typescript
const post = await getPost(postId)
if (post.authorId !== currentUser.id) {
return { success: false, error: 'Sem permissão' }
}
```
4. Execute a operação: só rode a lógica de negócio depois que todas as verificações passarem
Lembre-se: Server Actions não são mágica; as verificações de segurança precisam ser implementadas manualmente. - 5
Step 5: Melhorar a experiência do usuário
Adicione estado de carregamento e tratamento de erros:
1. Use useFormStatus no componente do botão:
```typescript
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return <button disabled={pending}>...</button>
}
```
2. Use revalidatePath para atualizar o cache:
```typescript
import { revalidatePath } from 'next/cache'
revalidatePath('/posts')
```
3. Atualização otimista (opcional, para operações com alta taxa de sucesso):
```typescript
const [optimisticState, setOptimisticState] = useOptimistic(initialState)
```
Boas práticas:
• Formulário com lógica complexa → use useActionState
• Componente de botão independente → use useFormStatus
• Depois de uma operação bem-sucedida → atualize o cache das páginas relacionadas
FAQ
Qual é a diferença entre Server Actions e API Routes? Quando usar cada uma?
Server Actions são seguras? Quais medidas de segurança são necessárias?
Qual é a diferença entre useActionState e useFormStatus?
Como passar parâmetros que não são campos do formulário?
Como atualizar os dados da página depois do envio do formulário?
Quando devo usar uma atualização otimista?
17 min de leitura · Publicado em: 19 dez 2025 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Busca de dados em Server Components do Next.js: fetch, banco de dados e boas práticas
Entenda como buscar dados em Server Components do Next.js com fetch ou consultas diretas ao banco, além de async/await, cache, tratamento de erros e armadilhas comuns.
Parte 5 de 26
Próximo
Login OAuth no Next.js: integração passo a passo com Google, GitHub e WeChat
Entenda o OAuth com uma analogia simples de retirada de encomenda e implemente login com Google, GitHub e WeChat no NextAuth.js, incluindo um guia completo de solução de erros.
Parte 7 de 26



Comentários
Entre com GitHub para comentar