Ir para o conteúdo
Nextjs

Next.js Middleware: Autenticação, A/B Testing e Geolocalização na Edge

Marcos Soares
Atualizado em 
13 minutos de leitura
Ilustracao 3D de um portal de vidro fosco interceptando feixes de luz coloridos representando middleware Next.js na edge
Ouça este artigo
0:00Next.js Middleware: Autenticação, A/B Testing e Geolocalização na Edge--:--

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.

Neste artigo

O que o Middleware do Next.js resolve (e onde ele roda)

Middleware no Next.js intercepta toda requisição antes que ela chegue a uma rota, API route ou asset estático. Ele roda no Edge Runtime da Vercel (ou em qualquer runtime compatível com a Web API Request/Response), não no Node.js tradicional. Essa distinção importa: você não tem acesso a fs, child_process, nem a bibliotecas que dependem de APIs nativas do Node.js.

O ganho concreto: latência baixa para decisões que precisam acontecer antes do render. Verificar se o usuário tem token válido, decidir qual variante de A/B testing servir, redirecionar por país. Tudo isso sem tocar no SSR, sem round-trip extra ao servidor de aplicação.

O custo: o Edge Runtime tem limites reais. Sem conexão direta a banco de dados (a menos que o driver suporte HTTP, como o Neon serverless driver ou o Planetscale serverless). Sem bibliotecas pesadas. O bundle do middleware tem limite de 1 MB na Vercel. Se a lógica que você precisa executar depende de queries complexas ou processamento pesado, middleware é o lugar errado.

CaracterísticaMiddleware (Edge Runtime)API Route (Node.js Runtime)Server Component
Executa antes do renderSimNãoNão
Acesso a fs, APIs nativasNãoSimSim
Latência típica< 5ms20-100msDepende do SSR
Limite de bundle~1 MB (Vercel)Sem limite práticoSem limite prático
Acesso a banco diretoApenas drivers HTTPSimSim
Rewrite/redirect sem renderSimParcialNão

Se você já trabalha com API Gateway patterns, o middleware do Next.js cumpre um papel similar para a camada de apresentação: interceptar, decidir e rotear.

Estrutura base do middleware

O middleware vive em middleware.ts na raiz do projeto (ou dentro de src/, se você usa essa convenção). Um único arquivo para toda a aplicação. Não existe middleware por rota como no Express. Você controla o escopo com o matcher no config.

TypeScript
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware(request: NextRequest) {
  return NextResponse.next();
}
 
// matcher evita que o middleware rode para assets estáticos e favicon
export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
};

O matcher usa regex negativa para excluir rotas que não precisam de interceptação. Sem isso, toda requisição de imagem, CSS e JS passaria pelo middleware, adicionando latência desnecessária.

Autenticação: verificação de JWT na edge

O caso mais comum. Verificar se o usuário tem um token válido antes de servir rotas protegidas. A lógica é simples: leia o cookie ou header, valide a assinatura do JWT, redirecione se inválido.

Para validação de JWT no Edge Runtime, você não pode usar jsonwebtoken (depende de APIs do Node.js). Use jose, que é compatível com Web Crypto API.

TypeScript
// lib/auth-edge.ts
import { jwtVerify } from "jose";
 
const JWT_SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET!
);
 
export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, JWT_SECRET);
    return { valid: true, payload };
  } catch {
    // jwtVerify lança erro para tokens expirados, malformados ou com assinatura inválida
    return { valid: false, payload: null };
  }
}
TypeScript
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { verifyToken } from "./lib/auth-edge";
 
const PROTECTED_PATHS = ["/dashboard", "/settings", "/api/user"];
 
function isProtectedPath(pathname: string): boolean {
  return PROTECTED_PATHS.some((path) => pathname.startsWith(path));
}
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  if (!isProtectedPath(pathname)) {
    return NextResponse.next();
  }
 
  const token = request.cookies.get("auth-token")?.value;
 
  if (!token) {
    const loginUrl = new URL("/login", request.url);
    // preserva a rota original para redirect pós-login
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }
 
  const { valid, payload } = await verifyToken(token);
 
  if (!valid) {
    const response = NextResponse.redirect(
      new URL("/login", request.url)
    );
    // limpa cookie inválido para evitar loop de redirect
    response.cookies.delete("auth-token");
    return response;
  }
 
  // injeta dados do usuário no header para consumo em Server Components
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-user-id", String(payload!.sub));
  requestHeaders.set("x-user-role", String(payload!.role));
 
  return NextResponse.next({
    request: { headers: requestHeaders },
  });
}
 
export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
};

O padrão de injetar dados via headers (x-user-id, x-user-role) evita uma segunda chamada de verificação dentro do Server Component. A página lê headers() e já tem o contexto do usuário. Se você usa Supabase como backend, o middleware é onde você valida a session do Supabase Auth antes de servir rotas protegidas.

A/B testing no middleware funciona assim: na primeira visita, o middleware sorteia uma variante, grava em cookie e faz rewrite para a rota correspondente. Nas visitas seguintes, lê o cookie e mantém a consistência.

A vantagem sobre A/B testing no client-side: zero flicker. O usuário já recebe a variante correta no primeiro byte. Sem useEffect que troca o conteúdo depois do render.

TypeScript
// lib/ab-testing.ts
 
export interface Experiment {
  name: string;
  variants: string[];
  // peso de cada variante (deve somar 1.0)
  weights: number[];
}
 
export const EXPERIMENTS: Record<string, Experiment> = {
  "pricing-page": {
    name: "pricing-page",
    variants: ["control", "variant-a", "variant-b"],
    weights: [0.5, 0.25, 0.25],
  },
};
 
export function pickVariant(experiment: Experiment): string {
  const random = Math.random();
  let cumulative = 0;
 
  for (let i = 0; i < experiment.variants.length; i++) {
    cumulative += experiment.weights[i];
    if (random <= cumulative) {
      return experiment.variants[i];
    }
  }
 
  // fallback para o último (cobertura de arredondamento de ponto flutuante)
  return experiment.variants[experiment.variants.length - 1];
}
TypeScript
// middleware.ts (seção de A/B testing integrada)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { EXPERIMENTS, pickVariant } from "./lib/ab-testing";
 
function handleAbTesting(
  request: NextRequest,
  response: NextResponse
): NextResponse {
  const { pathname } = request.nextUrl;
 
  if (pathname !== "/pricing") {
    return response;
  }
 
  const experiment = EXPERIMENTS["pricing-page"];
  const cookieName = `ab-${experiment.name}`;
  const existingVariant = request.cookies.get(cookieName)?.value;
 
  const variant = existingVariant ?? pickVariant(experiment);
 
  if (variant === "control") {
    // control serve a página original, sem rewrite
    if (!existingVariant) {
      response.cookies.set(cookieName, variant, {
        maxAge: 60 * 60 * 24 * 30, // 30 dias: tempo suficiente para o experimento rodar
        httpOnly: true,
        sameSite: "lax",
      });
    }
    return response;
  }
 
  // rewrite transparente: a URL não muda para o usuário
  const rewriteUrl = new URL(`/pricing/${variant}`, request.url);
  const rewriteResponse = NextResponse.rewrite(rewriteUrl);
 
  if (!existingVariant) {
    rewriteResponse.cookies.set(cookieName, variant, {
      maxAge: 60 * 60 * 24 * 30,
      httpOnly: true,
      sameSite: "lax",
    });
  }
 
  return rewriteResponse;
}

Para esse padrão funcionar, você precisa das páginas de variante no filesystem:

Text
app/
  pricing/
    page.tsx              ← control
    variant-a/
      page.tsx            ← variante A
    variant-b/
      page.tsx            ← variante B

Se o número de variantes cresce, considere usar estratégias de deploy como canary para testar variantes em infraestrutura separada, em vez de empilhar tudo no mesmo build.

Geolocalização: redirecionamento por país

A Vercel injeta headers de geolocalização automaticamente (x-vercel-ip-country, x-vercel-ip-city, x-vercel-ip-region). Em outros provedores, você precisa de um serviço externo ou do header cf-ipcountry da Cloudflare.

TypeScript
// lib/geo.ts
 
export interface GeoConfig {
  countryCode: string;
  redirectPath: string;
  // idioma padrão para o país
  locale: string;
}
 
export const GEO_REDIRECTS: GeoConfig[] = [
  { countryCode: "BR", redirectPath: "/pt-br", locale: "pt-BR" },
  { countryCode: "PT", redirectPath: "/pt-pt", locale: "pt-PT" },
  { countryCode: "ES", redirectPath: "/es", locale: "es" },
  // demais países caem no inglês (default)
];
 
export function findGeoRedirect(
  countryCode: string | null
): GeoConfig | undefined {
  if (!countryCode) return undefined;
  return GEO_REDIRECTS.find(
    (config) => config.countryCode === countryCode.toUpperCase()
  );
}
TypeScript
// middleware.ts (seção de geolocalização)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { findGeoRedirect } from "./lib/geo";
 
function handleGeoRedirect(request: NextRequest): NextResponse | null {
  const { pathname } = request.nextUrl;
 
  // evita loop: se já está em um path localizado, não redireciona de novo
  if (pathname.startsWith("/pt-br") || pathname.startsWith("/pt-pt") || pathname.startsWith("/es")) {
    return null;
  }
 
  // cookie de override permite que o usuário escolha idioma manualmente
  const localeOverride = request.cookies.get("locale-override")?.value;
  if (localeOverride) {
    return null;
  }
 
  // header da Vercel; troque por 'cf-ipcountry' na Cloudflare
  const country = request.headers.get("x-vercel-ip-country");
  const geoConfig = findGeoRedirect(country);
 
  if (!geoConfig) {
    return null;
  }
 
  const redirectUrl = new URL(geoConfig.redirectPath + pathname, request.url);
  const response = NextResponse.redirect(redirectUrl, 307);
 
  // 307 (temporary) em vez de 308: permite que o usuário mude de país/VPN sem cache permanente
  response.cookies.set("detected-locale", geoConfig.locale, {
    maxAge: 60 * 60 * 24 * 7,
    sameSite: "lax",
  });
 
  return response;
}

O 307 é intencional. Um 308 (permanent redirect) seria cacheado pelo browser, e se o usuário trocar de VPN ou viajar, ficaria preso no redirect antigo. Para conteúdo localizado com SSR, o middleware garante que o Server Component já receba a requisição na rota correta.

Composição: middleware unificado

Na prática, você precisa dos três comportamentos no mesmo arquivo. A composição funciona como uma pipeline: cada handler decide se atua ou passa adiante.

TypeScript
// middleware.ts (versão completa)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { verifyToken } from "./lib/auth-edge";
import { EXPERIMENTS, pickVariant } from "./lib/ab-testing";
import { findGeoRedirect } from "./lib/geo";
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // 1. Geolocalização (roda primeiro: redireciona antes de qualquer processamento)
  const geoResponse = handleGeoRedirect(request);
  if (geoResponse) return geoResponse;
 
  // 2. Autenticação (rotas protegidas)
  if (isProtectedPath(pathname)) {
    const authResponse = await handleAuth(request);
    if (authResponse) return authResponse;
  }
 
  // 3. A/B testing (rotas com experimento ativo)
  const response = NextResponse.next();
  return handleAbTesting(request, response);
}
 
// ... funções handleGeoRedirect, handleAuth, handleAbTesting
// como definidas nas seções anteriores
 
export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
};

A ordem importa. Geo primeiro, porque um redirect por país torna desnecessário verificar auth ou A/B na rota original. Auth segundo, porque um usuário não autenticado não deveria participar de experimentos em rotas protegidas.

O que NÃO fazer

Erro 1: fazer fetch a APIs externas lentas dentro do middleware.

TypeScript
// ERRADO: chamada de rede que adiciona latência a TODA requisição
export async function middleware(request: NextRequest) {
  const userData = await fetch("https://api.example.com/user/profile", {
    headers: { Authorization: `Bearer ${getToken(request)}` },
  });
  // se a API demora 200ms, toda página do site demora +200ms
  const user = await userData.json();
  // ...
}

O middleware roda em toda requisição que casa com o matcher. Uma chamada de 200ms aqui significa 200ms adicionais em cada navegação. Valide o JWT localmente (verificação de assinatura é operação de CPU pura, < 1ms) e deixe chamadas de rede para Server Components ou API routes.

TypeScript
// CORRETO: validação local do token, sem round-trip de rede
export async function middleware(request: NextRequest) {
  const token = request.cookies.get("auth-token")?.value;
  if (!token) return NextResponse.redirect(new URL("/login", request.url));
 
  // jwtVerify usa Web Crypto API, roda localmente no edge
  const { valid } = await verifyToken(token);
  if (!valid) return NextResponse.redirect(new URL("/login", request.url));
 
  return NextResponse.next();
}

Erro 2: não limitar o matcher e processar assets estáticos.

TypeScript
// ERRADO: sem matcher, o middleware roda para /favicon.ico, /_next/static/*, imagens...
export function middleware(request: NextRequest) {
  // lógica de auth rodando para cada chunk de JS carregado
}
// sem export const config

O impacto é multiplicado: uma página típica do Next.js carrega 15-30 assets estáticos. Sem matcher, são 15-30 execuções desnecessárias do middleware por page load.

Erro 3: A/B testing sem cookie persistente.

TypeScript
// ERRADO: sorteia variante em toda requisição
export function middleware(request: NextRequest) {
  const variant = Math.random() > 0.5 ? "a" : "b";
  // usuário vê variante A, recarrega, vê variante B
  return NextResponse.rewrite(new URL(`/pricing/${variant}`, request.url));
}

Sem persistência em cookie, o mesmo usuário oscila entre variantes. Os dados do experimento ficam inutilizáveis porque não há consistência por sessão.

Testando middleware localmente

O next dev emula o Edge Runtime, mas os headers de geolocalização não existem em localhost. Para testar geo, injete headers manualmente:

Bash
# simula requisição do Brasil
curl -H "x-vercel-ip-country: BR" http://localhost:3000/
 
# simula requisição da Espanha com cookie de auth
curl -H "x-vercel-ip-country: ES" \
     -b "auth-token=eyJhbGciOiJIUzI1NiJ9..." \
     http://localhost:3000/dashboard

Para testes automatizados, o Playwright permite setar headers customizados por contexto, o que facilita testar cenários de geo e auth em conjunto.

Quando middleware é a escolha errada

Middleware não substitui autorização no backend. Ele opera na camada de roteamento. Um token válido no middleware não garante que o usuário tem permissão para acessar um recurso específico. A segurança da API continua sendo responsabilidade da camada de backend.

Se a lógica de decisão precisa de dados do banco (verificar se o usuário é dono de um recurso, checar limites de plano), faça isso no Server Component ou na API route. O middleware cuida do gate binário: autenticado ou não, país A ou país B, variante X ou variante Y. Decisões que dependem de estado complexo pertencem a camadas com acesso completo ao runtime Node.js.

Para rate limiting pesado, um API Gateway dedicado com Redis é mais adequado. O middleware do Next.js não tem acesso nativo a stores de contagem distribuída (a menos que você use um KV store via HTTP, o que adiciona latência).

FAQ

O middleware do Next.js roda em toda requisição, mesmo para páginas estáticas (ISR/SSG)?

Sim. O middleware intercepta a requisição antes do cache da CDN na Vercel. Para páginas estáticas que não precisam de lógica de middleware, exclua-as no matcher para evitar execuções desnecessárias.

Posso usar variáveis de ambiente no middleware?

Sim, mas apenas variáveis prefixadas com NEXT_PUBLIC_ são acessíveis no Edge Runtime por padrão. Para variáveis privadas (como JWT_SECRET), elas ficam disponíveis via process.env no middleware porque ele roda no servidor, não no browser. A confusão surge porque o Edge Runtime não é o Node.js, mas o Next.js injeta as env vars corretamente.

Como debugar middleware em produção?

Na Vercel, os logs do middleware aparecem em "Functions" > "Edge". Use console.log com moderação: cada log gera custo em plataformas serverless. Para diagnóstico estruturado, injete um header de debug (x-middleware-debug) que o frontend pode ler em headers() dentro de Server Components.

Middleware substitui o NextAuth/Auth.js?

Não substitui, complementa. O NextAuth gerencia sessões, provedores OAuth e callbacks. O middleware consome o token que o NextAuth gerou para decidir se a requisição passa ou é redirecionada. A documentação do Auth.js inclui um exemplo oficial de integração com middleware.

Posso ter mais de um arquivo de middleware?

Não. O Next.js suporta apenas um middleware.ts por projeto. A composição de múltiplas responsabilidades (auth, A/B, geo) precisa acontecer dentro desse arquivo único, como mostrado na seção de composição. Se o arquivo cresce demais, extraia a lógica para módulos separados (lib/auth-edge.ts, lib/ab-testing.ts) e importe no middleware.

Marcos Soares

Escrito por

Marcos Soares

Fullstack Developer · CEO da Agência Poti

Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.

Comentários

Participe da discussão

Seja o primeiro a comentar!

Continue Aprofundando

Guias de integração relacionados

Conteúdo técnico toda semana

Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.

Sem spam. Cancele a qualquer momento com 1 clique.