Server Actions: processar formulários sem API separada
1. Visão geral
Antes, se você quisesse processar um formulário no Next.js, tinha que escrever uma API route (app/api/.../route.ts), depois chamar fetch do cliente para enviar dados. Server Actions eliminam esse passo intermediário: você escreve uma função que roda no servidor, anexa direto na propriedade action do <form>, pronto.
Lendo este artigo, você saberá como declarar um Server Action, usá-lo para formulários com validação, processar os estados pending/error com useFormState e useFormStatus, revalidar dados após o envio, e evitar alguns erros comuns ao migrar de API routes.
Nota: Server Actions é um recurso do App Router (Next.js 13.4+), estável desde Next.js 14. Se você ainda usa Pages Router, este artigo não se aplica.
2. Seu primeiro Server Action
Um Server Action é uma função async com a diretiva "use server", pode ser colocada em um arquivo separado ou direto no componente (se for um Server Component).
// app/actions/customer-actions.ts
"use server";
export async function createCustomer(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
console.log("Criando cliente:", name, email);
// Simulando salvamento no BD
}
// app/customers/new/page.tsx
import { createCustomer } from "@/app/actions/customer-actions";
export default function NewCustomerPage() {
return (
<form action={createCustomer}>
<input name="name" placeholder="Nome do cliente" />
<input name="email" placeholder="Email" />
<button type="submit">Criar</button>
</form>
);
}
"use server"no início do arquivo informa ao Next.js que todas as exportações neste arquivo são Server Actions, executam apenas no servidor, não são incluídas no JS enviado ao cliente.action={createCustomer}anexa a função direto ao formulário. Next.js serializa automaticamente o formulário emFormDatae chama a função no servidor ao enviar.- Não precisa de
onSubmit, não precisa depreventDefault, não precisa defetch. O formulário funciona até mesmo quando JavaScript ainda não foi carregado (progressive enhancement).
Ao enviar: o log aparece no terminal do servidor (não no console do navegador), porque a função roda no servidor.
3. Retornar resultados para o cliente com useFormState
Na prática, você precisa exibir erros de validação ou mensagens de sucesso. useFormState (de react-dom) ajuda o Server Action a retornar estado para o cliente.
// app/actions/customer-actions.ts
"use server";
type ActionResult = { success: boolean; message: string };
export async function createCustomer(
_prevState: ActionResult,
formData: FormData
): Promise<ActionResult> {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
if (!name || !email) {
return { success: false, message: "Falta nome ou email." };
}
if (!email.includes("@")) {
return { success: false, message: "Email inválido." };
}
// Simulando salvamento no BD
return { success: true, message: `Cliente ${name} criado com sucesso.` };
}
// app/customers/new/page.tsx
"use client";
import { useFormState } from "react-dom";
import { createCustomer } from "@/app/actions/customer-actions";
const initialState = { success: false, message: "" };
export default function NewCustomerForm() {
const [state, formAction] = useFormState(createCustomer, initialState);
return (
<form action={formAction}>
<input name="name" placeholder="Nome do cliente" />
<input name="email" placeholder="Email" />
<button type="submit">Criar</button>
{state.message && (
<p style={{ color: state.success ? "green" : "red" }}>
{state.message}
</p>
)}
</form>
);
}
useFormState(action, initialState)envolve o Server Action, retorna[state, formAction].stateé o valor retornado mais recente da action,formActioné a função que de fato é anexada a<form action={...}>.- O Server Action recebe como primeiro parâmetro
prevState— obrigatório quando usado comuseFormState, mesmo se não for utilizado. - Como há
useFormState(um React hook), este arquivo deve ser um Client Component, portanto precisa de"use client".
# Enviar sem email
> Falta nome ou email.
# Enviar com dados válidos
> Cliente Nguyễn Văn A criado com sucesso.
4. Estado pending com useFormStatus
Ao enviar, você quer desabilitar o botão e mostrar loading. useFormStatus lê o estado pending do formulário pai mais próximo — mas deve estar em um componente filho separado.
// app/customers/new/submit-button.tsx
"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Criando..." : "Criar"}
</button>
);
}
useFormStatusfunciona apenas quando o componente é renderizado dentro de um<form>. Se colocar<SubmitButton />no mesmo componente que contém<form>, ainda assim funciona porque React busca o formulário pai mais próximo na árvore do DOM, não no mesmo componente.pendingétrueenquanto o Server Action está rodando no servidor, ajudando você a desabilitar o botão e evitar envios duplos.
Nota: Não chame
useFormStatusdireto no componente que contém<form>esperando que ele conheça o estado do formulário — na verdade ainda funciona porque busca pela árvore do DOM, mas separar em um componente filho é a melhor prática para evitar confusão quando há múltiplos formulários na mesma página.
5. Validar dados com Zod
Validar manualmente com if/else rapidamente fica confuso. Use Zod para declarar um schema claramente.
<!-- Não se aplica — esta é uma dependência npm, veja abaixo -->
npm install zod
// app/actions/customer-actions.ts
"use server";
import { z } from "zod";
const customerSchema = z.object({
name: z.string().min(2, "Nome deve ter pelo menos 2 caracteres."),
email: z.string().email("Email inválido."),
});
type ActionResult = {
success: boolean;
message: string;
errors?: Record<string, string[]>;
};
export async function createCustomer(
_prevState: ActionResult,
formData: FormData
): Promise<ActionResult> {
const parsed = customerSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!parsed.success) {
return {
success: false,
message: "Dados inválidos.",
errors: parsed.error.flatten().fieldErrors,
};
}
return { success: true, message: `Cliente ${parsed.data.name} criado com sucesso.` };
}
safeParseretorna um objeto comsuccessedata/error, não lança exceção — apropriado para retornar erros ao cliente em vez de falhar a action.flatten().fieldErrorsproduz um objeto como{ name: ["Nome deve..."], email: [...] }, conveniente para exibir erros logo abaixo de cada input.- Schema definido uma vez, reutilizável tanto para validação do cliente (se necessário) quanto do servidor — evita duplicação de lógica.
6. Revalidar dados após o envio
Após criar um cliente, a página de lista de clientes (em cache pelo Next.js) precisa ser atualizada. Use revalidatePath ou revalidateTag.
// app/actions/customer-actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createCustomer(formData: FormData) {
const name = formData.get("name") as string;
// ... salvar no BD
revalidatePath("/customers");
redirect("/customers");
}
revalidatePath("/customers")limpa o cache dessa página. Na próxima visita, Next.js renderizará novamente com dados atualizados.redirectlança uma exceção especial que Next.js intercepta para redirecionar — chame apósrevalidatePath, e não coloque dentro detry/catchque engula o erro, pois isso bloqueará o redirect de funcionar.- Ambas as funções podem ser chamadas apenas dentro de Server Actions ou Server Components, não podem ser chamadas do cliente.
7. Chamar Server Action de um Client Component (fora de um formulário)
Um Server Action não precisa necessariamente estar anexado a um <form>. Você pode chamá-lo diretamente como uma função async, por exemplo em onClick.
// app/customers/[id]/delete-button.tsx
"use client";
import { useState } from "react";
import { deleteCustomer } from "@/app/actions/customer-actions";
export function DeleteButton({ customerId }: { customerId: string }) {
const [isDeleting, setIsDeleting] = useState(false);
async function handleClick() {
setIsDeleting(true);
await deleteCustomer(customerId);
setIsDeleting(false);
}
return (
<button onClick={handleClick} disabled={isDeleting}>
{isDeleting ? "Excluindo..." : "Excluir"}
</button>
);
}
// app/actions/customer-actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function deleteCustomer(id: string) {
console.log("Excluindo cliente id:", id);
revalidatePath("/customers");
}
- Um Server Action é na verdade construído por Next.js em um endpoint POST oculto, o cliente o chama via uma requisição de rede criada automaticamente — você não vê o código fetch, mas existe, pode ser visto na aba Network.
- Esse jeito é útil para botões de excluir, curtir, alternar estado — onde você não precisa de um
<form>completo.
8. Escrever testes para Server Actions
Como um Server Action é apenas uma função async que recebe FormData, você o testa como qualquer outra função unitária, sem precisar mockar HTTP.
npm install -D vitest
// app/actions/customer-actions.test.ts
import { describe, it, expect } from "vitest";
import { createCustomer } from "./customer-actions";
function buildFormData(data: Record<string, string>): FormData {
const fd = new FormData();
for (const [key, value] of Object.entries(data)) {
fd.append(key, value);
}
return fd;
}
describe("createCustomer", () => {
it("retorna erro quando email é inválido", async () => {
const formData = buildFormData({ name: "An", email: "khong-hop-le" });
const result = await createCustomer({ success: false, message: "" }, formData);
expect(result.success).toBe(false);
expect(result.errors?.email).toContain("Email inválido.");
});
it("sucesso com dados válidos", async () => {
const formData = buildFormData({ name: "An", email: "an@example.com" });
const result = await createCustomer({ success: false, message: "" }, formData);
expect(result.success).toBe(true);
expect(result.message).toContain("An");
});
});
buildFormDatasimulaFormDatacomo o navegador enviaria, sem precisar renderizar componentes ou mockar servidor.- Como um Server Action é uma função pura (recebe entrada, retorna saída), os testes rodam rápido, sem precisar iniciar o servidor dev Next.js.
Nota: Se o Server Action chamar
redirectourevalidatePath, o teste lançará um erro — porque essas funções funcionam apenas em um contexto de requisição real do Next.js. Separe a lógica de negócio (validação, cálculos) da parte que chamaredirect/revalidatePathpara que os testes sejam mais fáceis.
9. Erros comuns
Esquecer "use server" ou colocá-la no lugar errado. A diretiva deve estar no início do arquivo (ou no início da função, se declarada inline em um Server Component). Sem ela, Next.js trata a função como código comum, a inclui no bundle do cliente e causa erro ao executar.
Usar useFormState/useFormStatus em um Server Component. Esses dois hooks pertencem a react-dom e só rodam em um Client Component. Esquecer "use client" causará um erro de build.
Retornar objetos complexos (instâncias de classe, Date não serializado) de um Server Action. Os dados retornados devem ser serializáveis pela rede, como JSON. Retornar Date bruto às vezes causa erros silenciosos — converta para string primeiro.
Chamar Server Action para tudo, incluindo lógica que não precisa do servidor. Se é apenas uma validação simples que não toca no BD, processar no cliente será mais rápido, evita viagens de ida e volta desnecessárias.
Não ter fallback quando JavaScript falha. Server Actions suportam progressive enhancement, mas apenas se você usar <form action={...}> corretamente. Se você chamar preventDefault() e depois chamar a action manualmente, perde esse benefício.
10. Quando não usar Server Actions
Server Actions são adequados para formulários CRUD internos de seu app Next.js. Mas se você precisa:
- Uma API pública para aplicativos móveis ou terceiros chamarem — escreva um Route Handler (
route.ts) real, com versionamento e autenticação clara. - Streaming de resposta ou manipulação de uploads de arquivo muito grandes com controle detalhado — Route Handlers são mais flexíveis.
- Lógica que precisa rodar independentemente, reutilizável fora do contexto Next.js (por exemplo, worker, cron job) — separe em um serviço próprio, o Server Action só chama esse serviço.
11. Conclusão
Server Actions ajudam você a escrever formulários que processam dados no servidor sem precisar construir uma API route separada, reduz bastante o código repetitivo de fetch/JSON. Combine com useFormState para exibir erros, useFormStatus para indicar estado pendente, e revalidatePath para atualizar o cache, você tem um fluxo CRUD limpo que funciona até quando JavaScript ainda não foi carregado. Use Zod para validação clara, e lembre-se de separar a action em um Route Handler real se precisar de uma API pública para uso externo.