Formulários sem JavaScript com Server Actions no Next.js (App Router)
Por John Pires · 29 de março de 2026

Formulários sem JavaScript com Server Actions no Next.js (App Router)
1) O que é e por que importa
O problema que estamos resolvendo
Formulários na web tradicionalmente dependem de JavaScript para interceptar o evento submit, enviar uma requisição fetch/axios e atualizar a interface dinamicamente. Isso funciona, mas complica deploy, aumenta o bundle e cria dependências para operações básicas. Com Server Actions do Next.js e o App Router é possível submeter formulários diretamente para o servidor sem JavaScript no cliente — o browser envia a requisição padrão e você processa no servidor com funções tipadas e seguras.
Por que isso importa para iniciantes
Para quem está começando, reduzir a superfície de erro facilita entender o fluxo: o navegador envia FormData, o servidor lê, valida e responde. Você elimina código de cliente, evita bugs de sincronização e ainda mantém boas práticas de acessibilidade. Além disso, é um atalho prático para construir features que funcionam mesmo quando o usuário tem JS desativado.
Em termos de performance e arquitetura, usar Server Actions reduz o bundle do cliente e centraliza lógica crítica no servidor. Isso melhora tempo para primeiro conteúdo interativo para páginas simples e torna o fluxo mais previsível.
2) Conceitos fundamentais com analogias
O que é uma Server Action (analogia)
Imagine um formulário físico que você coloca em uma caixa de correio: o formulário vai direto para o escritório responsável, sem precisar de um mensageiro que faça pré-processamento. Uma Server Action é a função no servidor que abre essa correspondência, valida e registra a informação. O browser continua fazendo um POST padrão; a diferença é que essa função é definida no código do Next.js e ligada ao form via atributo action.
Como o App Router se encaixa (analogia)
O App Router é como um balcão de atendimento reorganizado: cada rota é um guichê que pode renderizar no servidor (Server Components) ou no cliente (Client Components). Ao colocar a action numa Server Component, você garante que o processamento acontece no servidor, exatamente onde a regra de negócio deve viver.
Conceitos-chave:
Server Component: componente renderizado no servidor;
Server Action: função marcada com 'use server' que pode ser passada ao atributo action do form;
FormData: objeto enviado pelo browser que a action recebe;
revalidatePath / revalidateTag: formas de atualizar páginas ISR/SSG após mutação.
Tip: Server Actions só funcionam com App Router e requerem configuração compatível do Next.js. Elas são ideais para mutações simples sem precisar de client-side JS.
3) Passo a passo com código comentado
Preparando a rota e a estrutura
Vamos criar uma rota /contact usando App Router. A estrutura mínima no diretório app/ será: app/contact/page.tsx. Essa página vai renderizar um form que usa uma Server Action definida no mesmo ou em um arquivo importado.
Importante: a Server Action precisa ser executada no servidor. Declaramos 'use server' dentro da função. O form HTML recebe a action diretamente: <form action={sendMessage}>.
Exemplo básico: form que apenas loga dados no servidor
O bloco abaixo mostra um componente server que exibe um form e uma server action que lê FormData e faz um console.log. Isso é para demonstrar o fluxo mínimo que permite submissão sem JS.
Contexto: este arquivo deve ficar em app/contact/page.tsx. O que faz: renderiza form simples. Depois do código: ao submeter no browser sem JS, a função sendMessage executa no servidor e registra os dados no terminal do servidor.
import React from 'react'
export default function ContactPage() {
// Server action declarada dentro do mesmo arquivo
async function sendMessage(formData: FormData) {
'use server'
const name = formData.get('name')?.toString() || ''
const message = formData.get('message')?.toString() || ''
// Processamento: aqui apenas logamos
console.log('Mensagem recebida:', { name, message })
// Depois de processar, você pode redirecionar ou apenas retornar
}
return (
Contato
Nome
Mensagem
</label>
<button type='submit'>Enviar</button>
</form>
</div>
)
}
</code></pre>
<p>O que acontece: ao submeter, o servidor executa sendMessage com um FormData representando os campos. Não há JavaScript no cliente para interceptar nada.</p>
<h2>4) Exemplo prático completo</h2>
<h3>Objetivo do exemplo</h3>
<p>Construiremos uma página de contato que:</p>
<ol>
<li>Recebe formulário sem JS;</li>
<li>Valida dados no servidor;</li>
<li>Grava em um arquivo JSON durante desenvolvimento;</li>
<li>Força revalidação da página de listagem usando revalidatePath (ISR).</li>
</ol>
<h3>Arquivos envolvidos</h3>
<p>Criaremos três arquivos no app/:</p>
<ul>
<li>app/contact/page.tsx — componente que exibe o form;</li>
<li>app/contact/actions.ts — exporta a server action que processa o form;</li>
<li>app/messages/page.tsx — lista mensagens, revalidada após nova submissão.</li>
</ul>
<p>Contexto: este exemplo usa APIs do Next.js: 'use server' em actions.ts e revalidatePath do next/cache. Em produção, evite gravar em arquivo; prefira banco de dados.</p>
<h3>Código: actions.ts (processa e grava)</h3>
<p>O que faz: recebe FormData, valida, acrescenta timestamp e grava em data/messages.json. Depois chama revalidatePath para atualizar a rota /messages.</p>
<pre><code>import { revalidatePath } from 'next/cache'
import { promises as fs } from 'fs'
import { join } from 'path'
const dataFile = join(process.cwd(), 'data', 'messages.json')
export async function sendContactMessage(formData: FormData) {
'use server'
const name = formData.get('name')?.toString() || ''
const message = formData.get('message')?.toString() || ''
// Validação simples
if (!name.trim() || !message.trim()) {
throw new Error('Nome e mensagem são obrigatórios')
}
// Ler arquivo atual
let list = []
try {
const raw = await fs.readFile(dataFile, 'utf-8')
list = JSON.parse(raw)
} catch (e) {
list = []
}
// Acrescenta e grava
const entry = { id: Date.now(), name, message, createdAt: new Date().toISOString() }
list.unshift(entry)
await fs.mkdir(join(process.cwd(), 'data'), { recursive: true })
await fs.writeFile(dataFile, JSON.stringify(list, null, 2), 'utf-8')
// Força revalidação da rota que lista mensagens
revalidatePath('/messages')
}
</code></pre>
<p>O que acontece: quando a action executa, o arquivo data/messages.json é atualizado e a página /messages é revalidada, refletindo a nova entrada.</p>
<h3>Código: contact/page.tsx (form que aponta para a action)</h3>
<p>O que faz: renderiza o form que chama a server action importada. Após o envio, o browser fará uma requisição normal; se quiser, você pode fazer redirect no servidor usando next/navigation, mas aqui mantemos simples.</p>
<pre><code>import { sendContactMessage } from './actions'
export default function ContactPage() {
return (
<main>
<h2>Contato</h2>
<form action={sendContactMessage} method='post'>
<div>
<label>
Nome
<input name='name' />
</label>
</div>
<div>
<label>
Mensagem
<textarea name='message' />
</label>
</div>
<button type='submit'>Enviar</button>
</form>
</main>
)
}
</code></pre>
<p>O que acontece: o browser envia a requisição POST para a rota implícita da Server Action. A action executa no servidor, atualiza o arquivo e revalida a página.</p>
<h3>Código: messages/page.tsx (lista com ISR)</h3>
<p>O que faz: lê data/messages.json e renderiza; configuramos a página para usar revalidação via revalidate (opcional) ou confiar em revalidatePath para forçar atualização.</p>
<pre><code>import { promises as fs } from 'fs'
import { join } from 'path'
export const revalidate = 10 // opcional: revalida a cada 10s
export default async function MessagesPage() {
const file = join(process.cwd(), 'data', 'messages.json')
let list = []
try {
const raw = await fs.readFile(file, 'utf-8')
list = JSON.parse(raw)
} catch (e) {
list = []
}
return (
<main>
<h2>Mensagens</h2>
<ul>
{list.map(m => (
<li key={m.id}>
<strong>{m.name}</strong> <small>({m.createdAt})</small>
<p>{m.message}</p>
</li>
))}
</ul>
</main>
)
}
</code></pre>
<p>O que acontece: quando sendContactMessage chama revalidatePath('/messages'), o Next.js atualiza a rota de listagem. Se você usar caching/ISR, o conteúdo será substituído sem exigir que o usuário recarregue manualmente.</p>
<h2>5) Erros comuns, armadilhas e dicas práticas</h2>
<h3>Erros comuns</h3>
<p>1) Declarar a action em um Client Component: a action precisa executar no servidor; se você tentar exportá-la de um Client Component, o comportamento pode falhar.</p>
<p>2) Usar APIs que não existem no servidor de produção (ex: escrever em disco em ambientes serverless): gravar em arquivo funciona em desenvolvimento; em Vercel/Serverless, prefira banco de dados ou object storage.</p>
<h3>Dicas práticas</h3>
<p><strong>Validação:</strong> sempre valide os dados no servidor. FormData é manipulável; não confie apenas em atributos HTML.</p>
<p><strong>Mensagens de erro:</strong> lance erros da Server Action e capture no componente que renderiza o formulário via try/catch durante render server-side, ou retorne um estado que mostre erro após redirect.</p>
<blockquote>Tip: se precisar de feedback imediato no cliente (ex: mostrar loading), combine Server Actions com um Client Component que dispara a action apenas para UX, mas mantenha a action suportando submissão direta sem JS.</blockquote>
<h2>6) Próximos passos para aprofundar</h2>
<h3>Recursos e práticas avançadas</h3>
<p>1) Integrar com um banco de dados (Postgres/Mongo) em vez de gravação em disco. Use bibliotecas server-side dentro da Server Action.</p>
<p>2) Implementar autenticação e autorização: actions podem ler cookies/headers para validar sessão antes de mutar dados.</p>
<h3>Experimentar revalidateTag e middleware</h3>
<p>Use revalidateTag quando quiser granularidade por conteúdo; middleware pode ser útil para bloquear rotas ou checar headers antes que a action execute, mas lembre-se: middleware roda antes da rota, não substitui validação dentro da action.</p>
<p>Exercício prático recomendado: transformar o exemplo acima para guardar dados em um banco SQLite local usando Prisma dentro da Server Action. Isso ajuda a entender conexões, transações e limites de tempo em ambientes serverless.</p>
<h2>Conclusão prática</h2>
<p>Server Actions no App Router permitem criar formulários que funcionam sem JavaScript no cliente. O padrão é direto: defina uma função com 'use server', passe-a como action no form e trate FormData no servidor. Esse fluxo reduz complexidade no cliente, melhora acessibilidade e facilita manutenção para funcionalidades simples de mutação.</p>
<p>Resumo rápido de implementação:</p>
<ul>
<li>Coloque a Server Action em um arquivo server (ou em uma Server Component);</li>
<li>Use <form action={myAction} method='post'>;</li>
<li>Valide no servidor e atualize dados com revalidatePath/revalidateTag se necessário.</li>
</ul>
<p>Prática imediata: pegue o exemplo completo e adapte para persistir em um DB. Evite gravação em disco para produção. Ao dominar esse fluxo, você terá uma alternativa robusta à abordagem tradicional de SPAs para formulários simples.</p>
<p>Implementando pequenos formulários dessa forma você ganha previsibilidade e reduz pontos de falha. Comece pequeno: formulário de contato, newsletter ou CRUD simples — e depois introduza client-side enhancements apenas quando necessário.</p>
</body>Assuntos: Next.js, app-router, server-actions, isr, ssr
Leia também
ISR on-demand escalável no Next.js App Router com webhooks, middleware e filas
Arquitetura prática para ISR on-demand no Next.js App Router: webhooks que enfileiram eventos em Redis, worker com retry/dedupe e endpoint interno que executa…
ISR on-demand com Server Actions: revalidação via webhook e middleware
Como migrar de endpoints de revalidate inseguros para uma arquitetura segura com Server Actions, webhooks assinados e middleware em Next.js App Router…
Preview CMS com Next.js App Router: Server Actions, Middleware e ISR na prática
Implementar preview CMS no Next.js App Router exige cookies seguros, middleware para marcar requisições e uso combinado de ISR e Server Actions. Neste artigo…
SSR multi-região no Next.js App Router: sincronizar ISR e invalidar caches
Guia prático para sincronizar ISR em implantações multi-região com Next.js App Router: padrões, Server Actions, endpoint de revalidate, broadcast entre…
Agrupamento de requisições no App Router com Server Actions e Edge Cache
Padrões práticos para agrupar requisições no App Router usando Server Actions e Edge Cache: batch endpoints, ISR (revalidate), retries, deduplicação e…
A/B testing no Next.js (App Router): middleware, cookies e ISR passo a passo
Guia prático para implementar A/B testing no Next.js App Router: use middleware para atribuir variantes, cookies para persistência e rewriting para habilitar…