Destino Dev

Blog

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