Destino Dev

Blog

Agrupamento de requisições no App Router com Server Actions e Edge Cache

Por John Pires · 14 de março de 2026

Agrupamento de requisições no App Router com Server Actions e Edge Cache

Cenario pratico do dia a dia

Problema comum em apps modernos

Em páginas do App Router é comum que um componente precise de múltiplos pedaços de dados vindos de APIs diferentes: detalhes do usuário, permissões, preferências e dados de terceiros. Se cada pedaço faz uma chamada separada ao backend ou a um serviço externo, você acaba com dezenas de requisições paralelas por renderização. Isso aumenta latência, custo de rede e risco de throttling.

Além disso, chamadas pequenas e frequentes tornam difícil aplicar caching eficiente. APIs de terceiros podem não suportar cache granular, e chamadas repetidas deterioram a experiência do usuário em conexões lentas.

O que queremos resolver

Objetivos práticos: reduzir número de requisições, agrupar chamadas quando possível, manter SSR/ISR previsível e aproveitar Edge Cache para respostas que podem ser servidas rápido. Tudo isso sem comprometer consistência nos dados sensíveis (por exemplo, páginas personalizadas).

Vamos focar em um padrão: usar Server Actions para coordenar agrupamento e orquestração, e Edge Cache + headers de revalidação para respostas públicas e semi-dinâmicas.

Arquitetura da solucao

Componentes principais

A solução proposta tem três camadas:

  • Server Action: função server-side dentro do App Router que recebe pedidos do cliente, agrupa IDs ou parâmetros e faz uma única chamada ao backend (ou batch endpoints).

  • Batching Layer: utilitário que consolida requisições, elimina duplicatas e aplica backoff/retries controlados.

  • Edge Cache / ISR: camada de cache na borda que mantém respostas agregadas por um tempo configurável (revalidate), com cache-control adequado.

Quem orquestra isso é a Server Action. O front envia uma lista de IDs ou uma intenção (por ex: "carregar dados para dashboard"), a server action decide se pode servir do cache de borda, se precisa agrupar e chamar um batch endpoint, ou se deve delegar a um worker assíncrono.

Trade-offs e delimitações

Não use esse padrão para dados estritamente user-specific que não podem ser cacheados. Para páginas com autenticação, combine com cache por sessão ou vary-by-cookie. A vantagem real aparece em dados parcialmente estáticos — catálogos, perfis públicos, metadados.

Edge Cache reduz latência, mas aumenta complexidade de invalidação. Decida políticas de revalidação (revalidate: 60, stale-while-revalidate) com cuidado e documente-as para o time de backend.

Codigo completo com explicacoes

Contexto: o que vamos implementar

Implementaremos um exemplo com três arquivos principais:

  1. app/actions/batchActions.ts — Server Action que agrupa requisições.

  2. app/utils/edgeCache.ts — helper para aplicar cache-control e revalidate.

  3. app/dashboard/page.tsx — componente que usa a Server Action via formulário ou fetch do servidor.

Cada bloco de código vem com explicação do que faz e o que acontece ao executar.

Server Action para agrupamento

Este arquivo é uma Server Action que aceita um array de ids e retorna dados em batch. Ele demonstra uso de fetch com opções next: { revalidate }, tratamento de erros e deduplicacao.

// app/actions/batchActions.ts
'use server';

import { headers } from 'next/headers';

export type Item = { id: string; name: string; updatedAt: string };

// Tempo de revalidação para ISR (em segundos)
export const REVALIDATE_SECONDS = 60;

// Função que faz a chamada batch ao backend. Use um endpoint que suporte POST com lista de ids.
export async function fetchItemsBatch(ids: string[]): Promise> {
  if (!ids || ids.length === 0) return {};

  const response = await fetch(process.env.API_URL + '/items/batch', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ids }),
    // Instrui Next.js a revalidar essa resposta após X segundos (ISR)
    next: { revalidate: REVALIDATE_SECONDS },
  });

  if (!response.ok) {
    const text = await response.text().catch(() => 'no body');
    throw new Error(`batch fetch failed: ${response.status} ${text}`);
  }

  const payload = await response.json();
  // payload deve ser um objeto: { items: [{id,name,updatedAt}, ...] }
  return Object.fromEntries(payload.items.map((it: Item) => [it.id, it]));
}

// Server Action que o App Router pode chamar via form/cliente server-side
export async function getItemsByIds(ids: string[]) {
  // Dedup
  const uniq = Array.from(new Set(ids));
  try {
    const map = await fetchItemsBatch(uniq);
    return uniq.map(id => map[id] ?? null);
  } catch (err) {
    // Re-lançar para que o componente superior trate
    throw err;
  }
}

O que faz: consolida ids, chama /items/batch e configura ISR via next.revalidate. O que acontece: Next armazenará essa resposta no cache de borda se disponível, e revalidará conforme configurado.

Edge Cache helper

Utilitário para forçar cabeçalhos de cache quando usamos rotas em app/api/.. ou rotas que retornam Response manualmente.

// app/utils/edgeCache.ts

export function cacheResponse(data: T, seconds = 60) {
  const headers = new Headers();
  // Cache público por X segundos, com stale-while-revalidate para melhorar p95
  headers.set('Cache-Control', `public, max-age=${seconds}, s-maxage=${seconds}, stale-while-revalidate=30`);
  return new Response(JSON.stringify(data), {
    status: 200,
    headers: headers,
  });
}

O que faz: monta uma Response com cabeçalhos de cache. O que acontece: em plataformas que respeitam Cache-Control na borda (Vercel, Cloudflare), a resposta será armazenada e servida sem tocar o backend na maior parte dos requests.

Componente do App Router usando a Server Action

Exemplo de página que solicita dados agrupados. A interação pode ser via formulário que dispara uma Server Action ou via fetch server-side em layout.

// app/dashboard/page.tsx
import { getItemsByIds } from '../actions/batchActions';
import { useState } from 'react';

export default async function DashboardPage() {
  // Exemplo: ids que precisamos carregar no servidor para SSR
  const ids = ['a', 'b', 'c'];
  // Chamada server-side: roda durante render no servidor
  const items = await getItemsByIds(ids);

  return (
      Dashboard
        {items.map((it, i) => (
           {it ? `${it.name} (${it.updatedAt})` : 'not found'} 
        ))}
  );
}

O que faz: durante render server-side a page chama a Server Action e recebe os dados agregados. O que acontece: se a chamada ao backend tiver cache configurado, a resposta será rápida e beneficiada pela borda; caso contrário a render espera pela resposta do batch.

Tratamento de erros e edge cases

Erros comuns e padrões de tratamento

Erros que veremos:

  • Backends retornando parcial success (alguns ids não encontrados).

  • Throttling ou 429 quando o batch é grande demais.

  • Inconsistência entre cache e dados ao atualizar registros.

Boas práticas:

  1. Retornar estrutura por id (map) para fácil montagem de resultados parciais.

  2. Implementar retries exponenciais para falhas transitórias, com jitter.

  3. Lidar com timeouts e fallback UI no cliente.

Exemplo: tratamento parcial e fallback

Implementação rápida de retry com limite e fallback para dados não encontrados.

// app/actions/retryHelpers.ts
export async function withRetry(fn: () => Promise, retries = 2, delayMs = 200): Promise {
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      if (attempt >= retries) throw err;
      attempt++;
      await new Promise(res => setTimeout(res, delayMs * Math.pow(2, attempt)));
    }
  }
}

O que faz: tenta executar fn e, em caso de erro, faz retries exponenciais. O que acontece: reduz a chance de uma falha temporária derrubar a render inteira.

Armadilhas com cache compartilhado

Uma armadilha frequente é servir conteúdo personalizado a partir de cache público. Use headers Vary e chaves lógicas para garantir que não exponha dados de um usuário a outro. Se a página for autenticada, prefira cache por-token ou cache local e não público.

Outra armadilha: revalidate baixo demais + alta latência de origem = picos de carga no backend. Ajuste revalidate considerando carga e tolerância de staleness.

Tip: se seu backend suporta endpoints batch, prefira-os para reduzir chamadas. Se não, implemente um worker que agregue requisições por janela de 50-200ms e faça um único request.

Performance e otimizacao

Medidas práticas para reduzir latência

Liste de ações imediatas:

  • Substituir N chamadas por um único /batch quando possível.

  • Usar next: { revalidate } em fetch para tirar proveito do ISR.

  • Ativar Edge Cache por meio de Cache-Control em rotas API e respostas estáticas.

  • Implementar deduplicação de ids antes do batch (Array.from(new Set(ids))).

Também considere: compactar payloads e solicitar apenas campos necessários (sparse fieldsets).

Medição e observabilidade

Para validar ganhos, meça:

  1. Tempo ao primeiro byte (TTFB) antes e depois de aplicar Edge Cache.

  2. Quantidade de chamadas ao backend por render de página.

  3. Taxa de 5xx/429 após deploy.

Instrumente métricas no batch layer (latência média do batch, tamanho médio de batch) e crie alarmes para crescimento inesperado.

Exemplo prático: worker de agregação por janela (pseudocódigo)

Em casos onde o backend não suporta batch, um padrão é agrupar requisições por uma janela curta (50-200ms). O código a seguir é um esboço server-side que você pode adaptar como singleton em uma rota Edge.

// app/actions/windowBatcher.ts (esboço)
const queue: { id: string; resolve: (v:any) => void; reject: (e:any) => void }[] = [];
let timer: NodeJS.Timeout | null = null;

function scheduleFlush() {
  if (timer) return;
  timer = setTimeout(async () => {
    timer = null;
    const batch = queue.splice(0, queue.length);
    const ids = batch.map(x => x.id);
    try {
      const res = await fetch(process.env.API_URL + '/items/batch', { method: 'POST', body: JSON.stringify({ ids }) });
      const data = await res.json();
      const map = Object.fromEntries(data.items.map((it:any) => [it.id, it]));
      for (const item of batch) {
        item.resolve(map[item.id] ?? null);
      }
    } catch (err) {
      for (const item of batch) item.reject(err);
    }
  }, 100); // agrupa 100ms
}

export function enqueue(id: string) {
  return new Promise((resolve, reject) => {
    queue.push({ id, resolve, reject });
    scheduleFlush();
  });
}

O que faz: agrupa solicitações que chegam num curto intervalo e faz um único POST. O que acontece: diminui número de chamadas quando muitos componentes pedem dados quase simultaneamente.

Conclusao

O padrão de agrupar requisições no App Router usando Server Actions e Edge Cache oferece redução significativa de latência e custo quando aplicado a dados parcialmente estáticos ou públicos. O ponto central é orquestrar requests no servidor (Server Actions), preferir endpoints batch e configurar cache/revalidate corretamente.

Implementar retries, deduplicação e fallback aumenta robustez. Para backends sem suporte a batch, use um window-batcher. Para dados sensíveis, evite cache público ou Vary conforme tokens.

Checklist prático antes de implementar em produção:

  1. Identificar dados agrupáveis e se o backend suporta batch.

  2. Definir política de revalidação (revalidate seconds) e stale behavior.

  3. Instrumentar métricas de chamadas ao backend, latência e erros.

  4. Garantir headers Cache-Control corretos nas rotas que retornam Response.

Aplicando esses padrões você reduz chamadas redundantes, melhora p95/p99 e mantém a arquitetura previsível. Comece com pequenos batches e monitore impacto antes de aumentar janela ou tempo de revalidação.

Assuntos: Next.js, app-router, server-actions, edge-cache, isr, Performance

Leia também