Alternar tema

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

Easton editorial illustration: API gateway workstation

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ísticaServer ActionsAPI Routes
UsoEnvio de formulários e alterações de dadosAPI RESTful e chamadas externas
Métodos HTTPApenas POSTGET, POST, PUT, DELETE etc.
Segurança de tiposNativaExige definição manual de tipos
Forma de chamadaChamada direta da funçãoRequisição com fetch
Cenário idealLógica interna e formuláriosAPI pública e integrações de terceiros
Quantidade de códigoMenorRelativamente 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:

  1. 'use server': informa ao Next.js que a função deve ser executada no servidor
  2. formData.get(): obtém o valor usando o atributo name do campo
  3. action={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:

  1. safeParse não lança exceções: quando a validação falha, retorna { success: false, error: ... }, permitindo tratar o erro de forma controlada
  2. flatten().fieldErrors: converte os erros para um formato como { name: ['erro1'], email: ['erro2'] }, fácil de exibir
  3. Retorno estruturado: inclui o indicador success e 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:

  1. O usuário envia o formulário → signup é chamada
  2. A validação falha no servidor → retorna { success: false, errors: {...} }
  3. useActionState armazena o resultado em state
  4. 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:

  • isPending do useActionState: ideal para uso dentro do componente do formulário
  • pending do useFormStatus: 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ísticaisPending do useActionStatepending do useFormStatus
Onde chamarDentro do componente do formulárioDentro de um componente filho do formulário
Uso idealQuando é preciso acessar o estado geral do formulárioBotão independente que só precisa saber se há um envio em andamento
FlexibilidadeFornece state e pending ao mesmo tempoFornece 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:

  1. 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.
  2. ID seguro para a Action: cada Action recebe um ID criptografado, difícil de enumerar.
  3. 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 fixosrevalidatePath
  • Dados distribuídos por várias páginasrevalidateTag

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:

  1. 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.

  2. 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.

  3. 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 useActionState e useFormStatus para 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. 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. 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. 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. 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. 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 adequadas para envios de formulários internos e alterações de dados. Elas aceitam apenas POST, oferecem segurança de tipos e exigem menos código. API Routes são mais indicadas para APIs RESTful externas, requisições GET e integrações com terceiros. Em resumo: use Server Actions internamente e API Routes externamente.
Server Actions são seguras? Quais medidas de segurança são necessárias?
Server Actions são executadas no servidor, mas as verificações de segurança precisam ser implementadas manualmente: 1) valide a entrada com Zod; 2) autentique o usuário verificando a sessão; 3) confirme que ele tem permissão para a operação. O framework oferece apenas proteção básica contra CSRF; não dependa dele para garantir toda a segurança automaticamente.
Qual é a diferença entre useActionState e useFormStatus?
O isPending do useActionState é usado dentro do componente do formulário e permite acessar state e pending ao mesmo tempo. O pending do useFormStatus precisa ser usado em um componente filho do formulário, como um botão, e fornece apenas o estado de envio. Use useActionState para lógica de formulário complexa e useFormStatus em botões independentes.
Como passar parâmetros que não são campos do formulário?
Há duas opções: 1) usar um campo oculto, <input type="hidden" name="postId" value={postId} />; 2) usar bind: const actionWithId = action.bind(null, postId) e depois <form action={actionWithId}>. O método bind costuma ser mais elegante.
Como atualizar os dados da página depois do envio do formulário?
Use revalidatePath para atualizar por caminho, como em revalidatePath('/posts'), ou revalidateTag para atualizar por tag, desde que a tag tenha sido definida anteriormente no fetch. Use revalidatePath quando houver poucos caminhos fixos e revalidateTag quando os dados estiverem distribuídos por várias páginas.
Quando devo usar uma atualização otimista?
A atualização otimista é adequada para operações com taxa de sucesso muito alta, como curtir ou favoritar. Com o Hook useOptimistic, a interface é atualizada imediatamente e o envio acontece em segundo plano. Se a operação tiver chance relevante de falhar, evite essa técnica, pois será necessário reverter a interface.

17 min de leitura · Publicado em: 19 dez 2025 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog