Alternar tema

Guia prático de Next.js Middleware: matcher, limites do Edge Runtime e armadilhas comuns

Easton editorial illustration: rendering-mode selector

Eu estava olhando os logs de erro da Vercel. O painel administrativo havia acabado de entrar em produção. Nos testes locais, todas as rotas sob /dashboard estavam protegidas corretamente: usuários não autenticados eram enviados para a página de login. Em produção, porém, alguém conseguiu abrir /dashboard/settings/profile, contornar a validação e acessar dados sensíveis.

Abri o código imediatamente. O arquivo de Middleware estava no lugar e a lógica parecia correta. Então, onde estava o problema?

Depois de meia hora na documentação do Next.js, encontrei a resposta na pequena seção sobre o matcher: eu havia escrito /dashboard/:path, que só corresponde a um nível, como /dashboard/settings. Caminhos mais profundos ficavam de fora. O correto era /dashboard/:path*. Aquele pequeno asterisco quase me colocou em uma situação complicada.

E não foi a primeira vez que tropecei no Middleware. Biblioteca incompatível com Edge Runtime, matcher que não funciona, loop infinito de redirecionamento… Já passei por quase todas essas armadilhas.

Se você usa Next.js Middleware, ou pretende aplicá-lo em autenticação, internacionalização ou testes A/B, este guia pode poupar bastante tempo. Vou destrinchar as configurações confusas, os erros recorrentes e três casos práticos completos.

Sem introduções genéricas sobre “o desenvolvimento web atual”. Vamos falar do problema real: como escrever, como evitar armadilhas e como fazer o Middleware funcionar de verdade.

O que é Middleware e por que usá-lo?

Em termos simples, o Middleware funciona como um posto de controle.

Antes de uma requisição chegar à sua página ou API, ela passa por esse posto. Ali você pode verificar a identidade do usuário, alterar a requisição ou até devolver uma resposta imediatamente — por exemplo, enviando um usuário não autenticado à página de login ou redirecionando visitantes para uma versão do site no idioma da região deles.

Ele roda no Edge Runtime, e isso é fundamental. Em vez de executar apenas no seu servidor, o código é implantado em pontos de presença próximos ao usuário, na borda da CDN. A distância é menor, a latência é baixa e o cold start fica perto de zero. Pense no Middleware como a camada de código mais próxima do usuário.

Qual é a diferença entre Edge Runtime e Node.js Runtime?

CaracterísticaEdge RuntimeNode.js Runtime
InicializaçãoCold start próximo de zeroPode levar centenas de milissegundos
Onde rodaPontos de presença globaisServidor específico
APIs disponíveisAPIs padrão da WebAPIs completas do Node.js
Uso indicadoLógica leve e resposta rápidaCálculo complexo e operações de banco de dados

Resumindo: é rápido, mas tem menos recursos. Você não pode usar módulos do Node.js como fs e path, nem conectar a maioria dos bancos de dados da forma tradicional. Esse é um dos pontos que mais gera problemas.

Quando usar Middleware?

Nem toda lógica deve ser colocada no Middleware. Estes são os cenários mais comuns:

1. Autenticação (Auth Gate)
É o uso clássico. Você verifica se o usuário está autenticado e, caso contrário, o envia para a página de login. Isso acontece antes de a requisição alcançar o servidor.

2. Internacionalização (i18n)
Com base na preferência de idioma — vinda da URL, de um cookie ou do navegador — você redireciona automaticamente para a versão correspondente: / → /pt ou /en.

3. Testes A/B
Você divide usuários entre duas variantes e mostra uma página diferente para cada grupo. Um cookie mantém a alocação estável para que a variante não mude a cada atualização.

4. Detecção de bots e limitação de requisições
Você bloqueia crawlers ou tráfego malicioso e limita a frequência de acesso de determinados IPs.

5. Logs e métricas
Você registra informações básicas de cada requisição, como caminho, origem e user agent, e as envia para um serviço de análise.

6. Reescrita de conteúdo (rewrite)
Você mapeia internamente a URL solicitada para outro caminho sem mudar o endereço exibido no navegador. Isso é útil em rotas dinâmicas e testes A/B.

Por que não fazer tudo diretamente no componente da página?

É possível, mas tende a ser mais lento. A lógica de Server Components ou Client Components só roda depois que a requisição chega ao servidor e, em alguns casos, depois do início da renderização. O Middleware intercepta a requisição na borda, responde antes e melhora a experiência.

Também há a vantagem de centralizar a lógica. Você não quer repetir a autenticação em toda página protegida. O Middleware resolve isso em um só lugar.

Mas não coloque tudo nele. Lógica de negócio complexa, consultas ao banco e cálculos pesados ainda pertencem a rotas de API ou componentes de servidor. O Middleware deve ser leve e rápido.

Configuração básica e estrutura do arquivo

Onde fica o arquivo?

O Next.js impõe uma regra rígida: o arquivo deve ficar na raiz do projeto ou dentro de src, com o nome middleware.ts ou middleware.js.

raiz-do-projeto/
├── app/
├── middleware.ts    ← coloque aqui
├── package.json

Se o projeto usa src:

raiz-do-projeto/
├── src/
│   ├── app/
│   ├── middleware.ts    ← coloque aqui
├── package.json

Atenção: um projeto só pode ter um arquivo middleware.ts. Não crie vários arquivos de Middleware dentro de app ou em outros diretórios. Faz sentido: esse arquivo é o “porteiro” global do projeto.

Como é o Middleware mais simples possível?

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  console.log('Nova requisição:', request.url);
  return NextResponse.next(); // libera e continua
}

É só isso. NextResponse.next() significa “pode continuar”, então a requisição chega normalmente à página ou API de destino.

APIs principais: NextRequest e NextResponse

NextRequest estende a Request padrão da Web e oferece propriedades úteis:

  • request.nextUrl: URL já interpretada, com acesso direto a pathname, search e outros campos
  • request.cookies: leitura e escrita mais simples de cookies
  • request.geo: localização geográfica do usuário, quando a plataforma de implantação oferece esse dado, como na Vercel

NextResponse oferece diferentes formas de resposta:

1. Continuar a requisição

return NextResponse.next();

2. Redirecionar para uma nova URL

return NextResponse.redirect(new URL('/login', request.url));

O endereço muda na barra do navegador.

3. Reescrever internamente, sem mudar a URL

return NextResponse.rewrite(new URL('/dashboard/v2', request.url));

O usuário acessa /dashboard e recebe o conteúdo de /dashboard/v2, mas continua vendo /dashboard no navegador. É útil em testes A/B e trocas de versão.

4. Retornar uma resposta diretamente

return new NextResponse('Acesso negado', { status: 403 });

O processamento para ali e o usuário recebe essa resposta.

Exemplo um pouco mais útil: adicionar um header personalizado

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Adiciona um header personalizado a todas as respostas
  response.headers.set('x-custom-header', 'my-value');

  return response;
}

Isso é comum quando você quer adicionar um timestamp a todas as respostas ou identificar a origem da requisição.

Observação sobre versões

Se você usa Next.js 15, fique atento: o nome middleware.ts foi alterado oficialmente para proxy.ts. O nome antigo ainda funciona por compatibilidade, mas projetos novos devem preferir o novo. Os exemplos deste artigo se aplicam ao Next.js 14 e 15; os principais conceitos e APIs permanecem os mesmos.

Correspondência de caminhos (matcher): onde mais aparecem erros

Sinceramente, foi no matcher que mais tive problemas.

Por que configurar um matcher?

Sem um matcher, o Middleware roda em todas as requisições, inclusive CSS, JavaScript, imagens e fontes. Se uma página carregar 20 arquivos estáticos, o Middleware poderá ser executado 20 vezes. Além de desperdiçar recursos, isso pode aumentar o tempo de resposta.

O matcher diz ao Next.js: “execute o Middleware apenas nestes caminhos; ignore os demais”.

Sintaxe básica

Exporte um objeto config no arquivo middleware.ts:

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*']
}

Assim, o Middleware só roda em caminhos iniciados por /dashboard ou /api.

O que significam *, + e ??

Esses modificadores controlam quantos segmentos podem corresponder:

* (zero ou mais)
/dashboard/:path* corresponde a:

  • /dashboard ✓
  • /dashboard/settings ✓
  • /dashboard/settings/profile ✓

+ (um ou mais)
/dashboard/:path+ corresponde a:

  • /dashboard ✗
  • /dashboard/settings ✓
  • /dashboard/settings/profile ✓

? (zero ou um)
/dashboard/:path? corresponde a:

  • /dashboard ✓
  • /dashboard/settings ✓
  • /dashboard/settings/profile ✗

Na maioria dos casos, * é suficiente.

Armadilhas comuns e soluções

Esta tabela resume erros que custaram bastante tempo. Vale guardar:

ProblemaForma erradaForma corretaMotivo
Rota dinâmica com vários níveis não corresponde/dashboard/:path/dashboard/:path*Sem *, apenas um nível corresponde
Caminho raiz fica de foramatcher: ['/dashboard/:path*']matcher: ['/', '/dashboard/:path*']/ não é incluído automaticamente
Recursos estáticos são interceptadosmatcher: ['/:path*']matcher: ['/((?!_next|favicon.ico).*)']É preciso excluir caminhos internos como _next
Rotas de API não ficam protegidasmatcher: ['/api/users']matcher: ['/api/:path*']Um caminho específico só cobre aquela rota

A armadilha mais comum: recursos estáticos

Você escreve este matcher:

export const config = {
  matcher: ['/:path*'] // tenta abranger todos os caminhos
}

Então os caminhos internos em _next/static também correspondem, o Middleware roda sem parar e a página fica lenta.

A solução correta é excluir caminhos com negative lookahead:

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

Essa expressão significa: “corresponda a todos os caminhos, exceto api, _next/static, _next/image e favicon.ico”.

Ela não é exatamente intuitiva. Também parti do exemplo oficial; você pode reutilizá-la sem precisar decorar a expressão.

Outra armadilha: valores dinâmicos não funcionam

const lang = 'pt'; // variável
export const config = {
  matcher: [`/${lang}/:path*`] // ❌ não funciona
}

O matcher precisa ser estático e conhecido durante a compilação. Não é possível inserir variáveis com template strings nem gerá-lo dinamicamente em runtime.

Se a decisão for dinâmica, coloque-a dentro da função middleware:

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Faça a decisão dinâmica aqui
  if (pathname.startsWith('/pt') || pathname.startsWith('/en')) {
    // lógica de processamento
  }

  return NextResponse.next();
}

// O matcher permanece estático
export const config = {
  matcher: ['/:locale/:path*']
}

Modelos de matcher que recomendo

Proteger rotas específicas, como o painel:

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*']
}

Corresponder a todas as rotas, exceto recursos estáticos:

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)',
  ],
}

Proteger todas as rotas de API:

export const config = {
  matcher: ['/api/:path*']
}

Talvez você também ache a seção sobre matcher na documentação do Next.js curta demais. Muitos detalhes acabam sendo descobertos na prática. Espero que esta tabela evite alguns desvios.

Limites do Edge Runtime e como contorná-los

Quando encontrei este problema pela primeira vez, fiquei realmente confuso.

Eu queria validar a identidade do usuário no Middleware e consultar o banco para confirmar se o token ainda era válido. Assim que rodei o código localmente, apareceu o erro: Native Node.js APIs are not supported in the Edge Runtime.

Como assim? Eu só queria acessar o banco de dados.

Depois entendi: o Edge Runtime não é um ambiente Node.js completo. Muitas APIs e bibliotecas comuns não estão disponíveis.

O que o Edge Runtime não oferece?

CategoriaAPI ou módulo incompatívelImpacto
Sistema de arquivosfs, pathNão é possível ler ou gravar arquivos locais
Subprocessoschild_processNão é possível executar comandos externos
CriptografiaAlgumas APIs de cryptoÉ preciso usar Web Crypto API
Banco de dadosDrivers nativos de MongoDB e MySQLA maioria dos drivers tradicionais não funciona
Outrosprocess.emit, setImmediateAlgumas APIs de baixo nível do Node.js não existem

Qual é o impacto prático?

Os efeitos mais diretos são:

  1. Você não pode consultar diretamente o banco para validar a identidade do usuário.
  2. Você não pode ler arquivos de configuração, como um config.json.
  3. Você não pode usar bibliotecas que dependem de APIs do Node.js.

Parece uma limitação grande, mas há formas adequadas de trabalhar com ela.

Estratégia: trate o Edge Runtime como uma linha de frente

A ideia central é simples: o Middleware toma decisões leves; etapas complexas ficam para as camadas seguintes.

Necessidade❌ Limite do Edge Runtime✅ Solução
Validar identidadeNão consulta o bancoValidar JWT localmente ou chamar uma rota de API
Criptografar e descriptografarParte de crypto não funcionaUsar Web Crypto API
Ler configuraçõesSem acesso ao sistema de arquivosUsar variáveis de ambiente (process.env) ou uma API
Registrar logsNão grava arquivos locaisEnviar dados a um serviço de logs, como Logtail
Operar no bancoDrivers tradicionais não funcionamUsar um banco compatível com Edge, como Vercel Postgres ou Supabase

Exemplo prático: validação de JWT

JWT, ou JSON Web Token, combina muito bem com Middleware porque é stateless: o próprio token contém as informações necessárias e não exige uma consulta ao banco.

import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose'; // compatível com Edge Runtime

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;

  // Sem token: redireciona para o login
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    // Valida o JWT com jose, compatível com Edge Runtime
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    // Validação concluída; adiciona o usuário ao header, se necessário
    const response = NextResponse.next();
    response.headers.set('x-user-id', payload.userId as string);

    return response;
  } catch (error) {
    // Token inválido: remove o cookie e redireciona
    const response = NextResponse.redirect(new URL('/login', request.url));
    response.cookies.delete('auth-token');
    return response;
  }
}

export const config = {
  matcher: ['/dashboard/:path*']
}

Pontos importantes:

  • Use jose em vez de jsonwebtoken, pois o segundo depende do módulo crypto do Node.js.
  • Leia o segredo do JWT por uma variável de ambiente; process.env funciona no Edge Runtime.
  • Remova o cookie quando a validação falhar para não repetir o mesmo erro em toda requisição.

E quando uma consulta ao banco for indispensável?

Alguns cenários realmente exigem uma consulta, por exemplo para verificar se uma conta foi suspensa. Nesse caso, o Middleware pode chamar uma rota de API:

export async function middleware(request: NextRequest) {
  const userId = request.cookies.get('user-id')?.value;

  if (!userId) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Chama uma rota de API para validar o estado do usuário
  const apiUrl = new URL('/api/check-user-status', request.url);
  const response = await fetch(apiUrl, {
    headers: { 'x-user-id': userId }
  });

  const { isActive } = await response.json();

  if (!isActive) {
    return NextResponse.redirect(new URL('/account-suspended', request.url));
  }

  return NextResponse.next();
}

A rota de API roda no Node.js Runtime e pode acessar o banco normalmente. Essa chamada aumenta a latência, portanto use-a apenas quando for necessária.

Sobre a Web Crypto API

Para criptografar ou gerar hashes, use a API nativa da Web:

// Gera um hash
const data = new TextEncoder().encode('hello world');
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

Essa API é menos prática que crypto do Node.js, mas é a opção disponível no Edge Runtime.

Como saber se uma biblioteca funciona no Edge Runtime?

Consulte a documentação ou teste diretamente. Se aparecer Native Node.js APIs are not supported, a biblioteca não é compatível.

Algumas opções populares têm versões próprias para Edge:

  • JWT: use jose em vez de jsonwebtoken
  • Banco de dados: Vercel Postgres, Supabase e suporte parcial do Prisma
  • Logs: Logtail e Axiom

Minha recomendação é não colocar trabalho complexo no Middleware. Ele deve entrar, decidir e sair rápido; deixe o processamento pesado para rotas de API.

Casos práticos: implementação completa de três cenários

Depois da teoria, vamos ao código. Estes três exemplos vêm de situações reais e podem ser adaptados diretamente.

Caso 1: autenticação e proteção de rotas

Cenário: você tem um painel administrativo e todas as páginas sob /dashboard exigem login.

Código completo:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Obtém o token
  const token = request.cookies.get('auth-token')?.value;

  // Sem login: redireciona e registra a URL original
  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname); // volta após o login
    return NextResponse.redirect(loginUrl);
  }

  try {
    // Valida o JWT
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    // Opcional: verifica se o token está perto de expirar
    const expiresAt = payload.exp as number;
    const now = Math.floor(Date.now() / 1000);
    const shouldRefresh = expiresAt - now < 3600; // menos de uma hora

    const response = NextResponse.next();

    if (shouldRefresh) {
      // Aqui você pode chamar uma rota de API para renovar o token
      // Omitido para manter o exemplo simples
      response.headers.set('x-token-refresh-needed', 'true');
    }

    // Envia dados do usuário à página, se necessário
    response.headers.set('x-user-id', payload.userId as string);
    response.headers.set('x-user-role', payload.role as string);

    return response;
  } catch (error) {
    // Token inválido ou expirado: remove e redireciona
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    loginUrl.searchParams.set('reason', 'expired');

    const response = NextResponse.redirect(loginUrl);
    response.cookies.delete('auth-token');

    return response;
  }
}

export const config = {
  matcher: ['/dashboard/:path*']
}

Pontos importantes:

  1. O parâmetro from registra o destino original e permite voltar após o login.
  2. A expiração é conferida com antecedência para renovar o token antes de o usuário perder a sessão no meio de uma tarefa.
  3. Os dados do usuário podem ser enviados por header e lidos pelo componente da página.

Como testar:

  • Apague os cookies do navegador e abra /dashboard → deve redirecionar para /login?from=/dashboard.
  • Faça login, defina o cookie e tente novamente → a página deve abrir normalmente.

Problema comum:

  • Sintoma: funciona localmente, mas não depois da implantação.
  • Causa: JWT_SECRET não foi configurado no ambiente de produção.
  • Solução: adicione a variável nas configurações da Vercel, Netlify ou da plataforma usada.

Caso 2: redirecionamento de rotas para internacionalização (i18n)

Cenário: seu site oferece vários idiomas e, ao acessar /, o usuário deve ser enviado a /pt ou /en conforme a preferência.

Código completo:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const supportedLocales = ['en', 'pt', 'ja'];
const defaultLocale = 'en';

function getPreferredLocale(request: NextRequest): string {
  // Prioridade 1: parâmetro da URL, para troca manual
  const urlLocale = request.nextUrl.searchParams.get('lang');
  if (urlLocale && supportedLocales.includes(urlLocale)) {
    return urlLocale;
  }

  // Prioridade 2: cookie da última escolha
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && supportedLocales.includes(cookieLocale)) {
    return cookieLocale;
  }

  // Prioridade 3: idioma do navegador no header Accept-Language
  const acceptLanguage = request.headers.get('accept-language');
  if (acceptLanguage) {
    // Interpretação simples de "pt-BR,pt;q=0.9,en;q=0.8"
    const browserLang = acceptLanguage.split(',')[0].split('-')[0];
    if (supportedLocales.includes(browserLang)) {
      return browserLang;
    }
  }

  return defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Verifica se o caminho já tem um prefixo de idioma
  const pathnameHasLocale = supportedLocales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (!pathnameHasLocale) {
    // Sem prefixo: redireciona para o caminho com idioma
    const locale = getPreferredLocale(request);
    const newUrl = new URL(`/${locale}${pathname}`, request.url);

    // Mantém os parâmetros da consulta
    newUrl.search = request.nextUrl.search;

    const response = NextResponse.redirect(newUrl);

    // Guarda a escolha por 30 dias
    response.cookies.set('NEXT_LOCALE', locale, {
      maxAge: 60 * 60 * 24 * 30,
      path: '/'
    });

    return response;
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    // Abrange todos os caminhos, exceto recursos estáticos e APIs
    '/((?!api|_next/static|_next/image|favicon.ico|.*\\.).*)'
  ]
}

Pontos importantes:

  1. Há três níveis de detecção: parâmetro da URL > cookie > configuração do navegador.
  2. O cookie preserva a escolha e a reaplica na próxima visita.
  3. O matcher exclui recursos estáticos para impedir que imagens também sejam redirecionadas.

Integração com next-intl:

Com next-intl, boa parte da lógica fica mais simples:

import { createI18nMiddleware } from 'next-intl/middleware';

export default createI18nMiddleware({
  locales: ['en', 'pt', 'ja'],
  defaultLocale: 'en'
});

export const config = {
  matcher: ['/((?!api|_next|.*\\.).)']
};

O next-intl cuida automaticamente da detecção de idioma e do gerenciamento de cookies.

Caso 3: teste A/B e feature flags

Cenário: você redesenhou a página inicial e quer mostrar a nova versão a 50% dos usuários antes de decidir pelo lançamento completo.

Código completo:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // O teste A/B só roda na página inicial
  if (pathname !== '/') {
    return NextResponse.next();
  }

  // Verifica se o usuário já foi alocado
  let variant = request.cookies.get('ab-test-homepage')?.value;

  if (!variant) {
    // Novo usuário: distribui aleatoriamente entre A e B
    variant = Math.random() < 0.5 ? 'A' : 'B';
  }

  let response: NextResponse;

  if (variant === 'B') {
    // Grupo B: reescreve para a nova versão sem mudar a URL
    response = NextResponse.rewrite(new URL('/homepage-v2', request.url));
  } else {
    // Grupo A: usa a versão original
    response = NextResponse.next();
  }

  // Mantém o grupo por sete dias
  response.cookies.set('ab-test-homepage', variant, {
    maxAge: 60 * 60 * 24 * 7,
    path: '/'
  });

  // Marca o grupo no header para análise
  response.headers.set('x-ab-variant', variant);

  return response;
}

export const config = {
  matcher: ['/']
}

Pontos importantes:

  1. Use rewrite, não redirect, para que o usuário continue vendo / na barra de endereço.
  2. O cookie mantém o grupo estável e evita mudanças de versão ao atualizar a página.
  3. O header identifica a variante para a ferramenta de análise.

Sugestão de instrumentação:

Leia o header no componente da página:

// app/page.tsx
import { headers } from 'next/headers';

export default function HomePage() {
  const headersList = headers();
  const abVariant = headersList.get('x-ab-variant');

  // Envia o evento de análise
  useEffect(() => {
    analytics.track('page_view', {
      page: 'homepage',
      variant: abVariant
    });
  }, [abVariant]);

  return <div>...</div>;
}

Assim, você consegue comparar a taxa de conversão dos grupos A e B no painel de analytics.

Versão avançada: alocação por ID de usuário, não por sorteio

Para que o mesmo usuário veja a mesma variante em dispositivos diferentes:

const userId = request.cookies.get('user-id')?.value;

if (userId) {
  // O hash do ID mantém uma alocação estável
  const hash = simpleHash(userId);
  variant = hash % 2 === 0 ? 'A' : 'B';
} else {
  // Para visitantes anônimos, usa cookie
  variant = request.cookies.get('ab-test-homepage')?.value ||
            (Math.random() < 0.5 ? 'A' : 'B');
}

// Função simples de hash
function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash);
}

Esses três casos cobrem os usos mais frequentes do Middleware. Você também pode combiná-los, como adicionar internacionalização a uma rota que já exige autenticação.

Otimização de desempenho e boas práticas

Escrever um Middleware é fácil; escrevê-lo bem exige alguns cuidados.

Organize a lógica em módulos

À medida que o projeto cresce, concentrar toda a lógica em middleware.ts vira uma confusão. O Next.js permite apenas um arquivo de entrada, mas você pode dividir o comportamento em funções e módulos separados.

Estrutura recomendada:

raiz-do-projeto/
├── middleware/
│   ├── auth.ts        # lógica de autenticação
│   ├── i18n.ts        # lógica de internacionalização
│   ├── ab-test.ts     # lógica de teste A/B
│   └── rate-limit.ts  # limitação de requisições
├── middleware.ts      # arquivo de entrada

Exemplo do arquivo de entrada:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { checkAuth } from './middleware/auth';
import { handleI18n } from './middleware/i18n';
import { handleABTest } from './middleware/ab-test';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. Trata a internacionalização primeiro
  const i18nResponse = handleI18n(request);
  if (i18nResponse) return i18nResponse;

  // 2. Verifica a autenticação
  if (pathname.startsWith('/dashboard')) {
    const authResponse = await checkAuth(request);
    if (authResponse) return authResponse;
  }

  // 3. Por fim, trata o teste A/B
  if (pathname === '/') {
    return handleABTest(request);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}

Exemplo de auth.ts:

// middleware/auth.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

export async function checkAuth(request: NextRequest): Promise<NextResponse | null> {
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    await jwtVerify(token, secret);
    return null; // validação aprovada; null significa continuar
  } catch {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

Assim, cada módulo tem uma responsabilidade clara e fica mais fácil de alterar.

Estratégia de cache: evite cálculos repetidos

Algumas decisões podem ser armazenadas para não serem refeitas em toda requisição. Uma verificação de permissões, por exemplo, talvez não precise ser repetida enquanto o token continuar válido.

Configuração em cache com Vercel Edge Config:

import { get } from '@vercel/edge-config';

export async function middleware(request: NextRequest) {
  // Lê feature flags já distribuídas pelo Edge Config
  const featureFlags = await get('feature-flags');

  if (featureFlags?.newDashboard) {
    return NextResponse.rewrite(new URL('/dashboard-v2', request.url));
  }

  return NextResponse.next();
}

Edge Config é um armazenamento de chave e valor distribuído globalmente pela Vercel. A leitura é rápida e funciona bem para configurações que mudam pouco.

Evite headers grandes demais

Headers definidos no Middleware são anexados à resposta. Se forem numerosos ou grandes, podem provocar o erro 431 Request Header Fields Too Large.

Recomendações:

  • Mantenha o total dos headers abaixo de 8 KB.
  • Envie apenas o necessário; não coloque o objeto completo do usuário no header.
  • Quando precisar transportar muitos dados, avalie usar um token criptografado.

Forma errada:

// ❌ Não faça isso
response.headers.set('x-user-data', JSON.stringify(userData)); // pode ficar enorme

Forma correta:

// ✅ Envie somente os campos essenciais
response.headers.set('x-user-id', user.id);
response.headers.set('x-user-role', user.role);

Otimize o matcher: correspondências precisas são melhores

Quanto mais específico for o matcher, menos trabalho desnecessário o Next.js fará.

Forma ampla demais:

export const config = {
  matcher: ['/:path*'] // corresponde a tudo
}

Forma melhor:

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'] // somente o necessário
}

Se o Middleware só protege algumas rotas, não use um curinga para executar em todas.

Monitoramento e depuração

Depuração no ambiente de desenvolvimento:

export function middleware(request: NextRequest) {
  if (process.env.NODE_ENV === 'development') {
    console.log('Middleware executado:', {
      path: request.nextUrl.pathname,
      method: request.method,
      cookies: request.cookies.getAll()
    });
  }

  // Sua lógica...
}

Logs em produção:

O console.log do Edge Runtime aparece nos logs da plataforma, como Edge Function Logs da Vercel. Não exagere: muitos registros aumentam o tempo de execução.

Estratégia recomendada: envie erros a um serviço especializado.

import { Logger } from '@logtail/edge';

const logger = new Logger(process.env.LOGTAIL_TOKEN);

export async function middleware(request: NextRequest) {
  try {
    // Sua lógica...
  } catch (error) {
    // Registra apenas quando houver erro
    await logger.error('Middleware error', {
      path: request.nextUrl.pathname,
      error: error.message
    });
    throw error;
  }
}

Checklist de boas práticas

Estas são as lições mais úteis que tirei dos problemas que já enfrentei.

✅ Faça:

  • Mantenha a lógica do Middleware leve e deixe tarefas complexas para rotas de API.
  • Use o matcher para abranger apenas os caminhos necessários.
  • Prefira JWT para autenticação e evite consultas ao banco.
  • Separe o código em módulos por responsabilidade.
  • Registre informações de depuração no ambiente de desenvolvimento.
  • Defina uma duração adequada para cookies.

❌ Evite:

  • Cálculos pesados ou operações de banco no Middleware.
  • Deixar o matcher sem configuração e executar em toda requisição.
  • Bibliotecas que dependem de APIs do Node.js.
  • Headers maiores que 8 KB.
  • Grande volume de logs em produção.
  • Loops infinitos de redirecionamento; sempre confira se o destino será interceptado novamente.

Seguindo esses princípios, a maioria dos problemas desaparece.

Erros comuns e técnicas de depuração

Por fim, vamos aos erros que mais fazem perder tempo. Já encontrei todos eles e alguns demoraram bastante para serem diagnosticados.

Erro 1: o Middleware não é executado

Sintoma: o arquivo existe, mas parece não ter efeito e a requisição segue normalmente.

Causas e soluções possíveis:

CausaComo verificarSolução
Arquivo no local erradoConfirme que middleware.ts está na raiz ou em srcMova o arquivo para o local correto
Matcher não cobre o caminhoRegistre request.nextUrl.pathnameAjuste a configuração do matcher
Erro de sintaxeConfira erros de compilação no consoleCorrija a sintaxe
Função exportada incorretamenteConfirme o uso de export function middlewareCorrija a exportação
Cache antigoLimpe o diretório .nextRode rm -rf .next && npm run dev

Dica de depuração:

Adicione um log no início da função:

export function middleware(request: NextRequest) {
  console.log('🔥 Middleware executado! Caminho:', request.nextUrl.pathname);
  // restante da lógica...
}

Se nem essa linha aparecer, o Middleware não está sendo executado; confira as causas da tabela.

Erro 2: Native Node.js APIs are not supported in the Edge Runtime

Sintoma: o erro aparece em runtime e aponta para uma API ou módulo incompatível.

Como localizar:

Leia a stack trace e descubra qual biblioteca ou trecho chamou uma API do Node.js. Os culpados mais comuns são:

  • módulos de sistema de arquivos, como fs e path
  • jsonwebtoken, que deve ser substituído por jose
  • drivers nativos de MongoDB e MySQL, que precisam de alternativas compatíveis com Edge

Soluções:

  1. Procure uma alternativa: consulte a documentação para saber se há uma versão compatível com Edge Runtime.
  2. Leve a lógica para uma rota de API: se a API do Node.js for indispensável, não execute o trecho no Middleware.
  3. Use APIs padrão da Web: substitua crypto do Node.js por Web Crypto API, por exemplo.

Exemplo:

// ❌ Não funciona
import jwt from 'jsonwebtoken';

// ✅ Use isto
import { jwtVerify } from 'jose';

Erro 3: Invalid middleware found

Sintoma: a mensagem aparece ao iniciar o projeto.

Causas possíveis:

1. Matcher vazio

// ❌ Não funciona
export const config = {
  matcher: []
}

2. A função middleware não foi exportada corretamente

// ❌ Não funciona
const middleware = (request: NextRequest) => { ... }

// ✅ Exporte a função
export function middleware(request: NextRequest) { ... }

3. A função não retorna um valor

// ❌ Não funciona
export function middleware(request: NextRequest) {
  console.log('Executa alguma lógica');
  // faltou return
}

// ✅ Sempre retorne uma resposta
export function middleware(request: NextRequest) {
  return NextResponse.next();
}

Erro 4: loop infinito de redirecionamento

Sintoma: o navegador mostra ERR_TOO_MANY_REDIRECTS e a página não abre.

Causa: a URL de destino também passa pelo Middleware e dispara o mesmo redirecionamento.

Cenário comum:

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token');

  if (!token) {
    // ❌ /login também passa por este Middleware
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/:path*'] // inclui /login
}

Solução: exclua a página de login e outras páginas públicas.

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // ✅ Libera primeiro as páginas públicas
  if (pathname === '/login' || pathname === '/') {
    return NextResponse.next();
  }

  const token = request.cookies.get('auth-token');

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

Outra opção é restringir o matcher:

export const config = {
  matcher: ['/dashboard/:path*'] // protege apenas as rotas privadas
}

Erro 5: a variável de ambiente está indisponível

Sintoma: process.env.XXX retorna undefined.

Causas:

  1. O arquivo .env.local não existe ou o nome da variável está incorreto.
  2. A variável não foi configurada na plataforma de implantação, como Vercel ou Netlify.
  3. O nome não segue as regras do Next.js.

Solução no desenvolvimento local:

// .env.local
JWT_SECRET=your-secret-here

Solução em produção:

Adicione a variável nas configurações do projeto na Vercel ou Netlify e faça uma nova implantação.

Atenção:

  • Reinicie o servidor de desenvolvimento após mudar variáveis de ambiente.
  • Variáveis da plataforma ficam disponíveis apenas no servidor e no Edge Runtime; no cliente, precisam do prefixo NEXT_PUBLIC_.

Resumo das técnicas de depuração

1. Use um header para confirmar a execução

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Indica que a requisição passou pelo Middleware
  response.headers.set('x-middleware-executed', 'true');
  response.headers.set('x-middleware-path', request.nextUrl.pathname);

  return response;
}

Abra as ferramentas de desenvolvimento do navegador e confira os headers da resposta.

2. Comente o código por etapas para localizar o problema

Quando não souber qual trecho causa o erro, desative partes da lógica progressivamente:

export function middleware(request: NextRequest) {
  console.log('Etapa 1');
  // ... alguma lógica

  console.log('Etapa 2');
  // ... mais lógica

  console.log('Etapa 3');
  return NextResponse.next();
}

O último log exibido mostra até onde a execução chegou.

3. Consulte Edge Function Logs na Vercel

Se o projeto está na Vercel, a guia Functions mostra os logs das Edge Functions, inclusive saídas de console.log e mensagens de erro.

4. Use next dev --turbo nos testes locais

Next.js 15 ou superior oferece o modo Turbo, que inicia mais rápido e costuma apresentar mensagens de erro mais claras:

npm run dev -- --turbo

Ao encontrar um problema, siga essas etapas com calma. Se ainda não resolver, pesquise a mensagem exata nas GitHub Discussions do Next.js ou no Stack Overflow; é provável que outra pessoa já tenha passado pelo mesmo caso.

Conclusão

Tudo o que vimos pode ser resumido em três pontos.

1. Conheça os limites de uso do Middleware

Não coloque qualquer lógica ali. O Middleware é apropriado para decisões e encaminhamentos leves, como autenticação, redirecionamento de rotas e testes A/B. Lógica de negócio complexa, consultas ao banco e cálculos intensos ficam em rotas de API ou componentes de servidor. Ele deve entrar e sair rápido, como um bom porteiro, não tentar administrar o prédio inteiro.

2. O matcher merece atenção especial

Essa é a área com mais armadilhas. Em rotas dinâmicas, lembre-se de *; exclua recursos estáticos e não intercepte páginas públicas, como o login. Em caso de dúvida, use os modelos deste guia ou registre os caminhos no ambiente de desenvolvimento.

3. Entenda e aceite os limites do Edge Runtime

O Edge Runtime não é um Node.js completo. Essa é uma escolha de arquitetura: abre mão de algumas capacidades para oferecer velocidade e distribuição global. Quando você sabe quais APIs não funcionam e como substituí-las com JWT, Web Crypto API ou chamadas a rotas de API, consegue aproveitar bem o ambiente.

Next.js Middleware não é difícil, mas tem muitos detalhes. Regras do matcher, limites do Edge Runtime e loops de redirecionamento estão entre os problemas mais frequentes. Escrevi este guia para que você não precise descobrir tudo da maneira mais demorada.

Se o seu projeto precisa interceptar requisições de forma global, experimente o Middleware. Primeiro faça o fluxo funcionar, depois otimize. Quando algo der errado, volte à seção de depuração e siga os testes passo a passo.

Se este artigo ajudou, compartilhe com quem também trabalha com Next.js. Pretendo abordar Server Actions no próximo texto — outro assunto cheio de detalhes que merecem atenção.

Boa sorte, e que seu Middleware funcione logo na primeira tentativa.

Processo completo de configuração do Next.js Middleware

Da criação do arquivo de Middleware à proteção de rotas, internacionalização e testes A/B.

⏱️ Estimated time: 3 hr

  1. 1

    Step 1: Criar o arquivo de Middleware

    Crie o arquivo:
    • Local: middleware.ts, na raiz do projeto
    • Exporte o objeto config para configurar o matcher
    • Exporte a função middleware para processar a requisição

    Estrutura básica:
    export const config = {
    matcher: '/dashboard/:path*'
    }

    export function middleware(request: NextRequest) {
    // lógica de processamento
    }
  2. 2

    Step 2: Configurar as regras do matcher

    Regras de correspondência:
    • Caminho único: '/dashboard'
    • Rota dinâmica: '/dashboard/:path*', com asterisco para vários níveis
    • Vários caminhos: ['/dashboard/:path*', '/admin/:path*']
    • Exclusão de caminhos: use negative lookahead, como '/((?!api|_next/static|_next/image|favicon.ico).*)'

    Atenção:
    • Rotas dinâmicas precisam de * para corresponder a vários níveis
    • Exclua recursos estáticos, como _next/static e _next/image
    • Não intercepte páginas públicas, como a tela de login
  3. 3

    Step 3: Implementar proteção de rotas e autenticação

    Etapas:
    1. Ler o token do cookie
    2. Verificar se o token é válido, com JWT ou uma chamada de API
    3. Redirecionar quem não está autenticado para a página de login
    4. Permitir que usuários autenticados continuem

    Pontos importantes:
    • Use NextRequest.cookies para obter cookies
    • Use NextResponse.redirect para redirecionar
    • Use NextResponse.next para continuar a requisição
    • Evite loops infinitos de redirecionamento
  4. 4

    Step 4: Implementar internacionalização e troca de idioma

    Etapas:
    1. Detectar a preferência de idioma por cookie, header ou valor padrão
    2. Verificar pelo caminho se é preciso redirecionar
    3. Adicionar o prefixo de idioma à URL
    4. Definir o cookie de idioma

    Pontos importantes:
    • Use request.headers.get('accept-language')
    • Use request.nextUrl.pathname para obter o caminho
    • Use NextResponse.rewrite para reescrever a URL
    • Permita a troca de idioma sem alterar a estrutura da URL
  5. 5

    Step 5: Lidar com os limites do Edge Runtime

    Limites e soluções:
    • APIs do Node.js não são compatíveis → use APIs padrão da Web
    • Não há sistema de arquivos → use variáveis de ambiente ou chamadas de API
    • Alguns pacotes npm não funcionam → confira a compatibilidade com Edge Runtime
    • Para validar JWT → use Web Crypto API ou uma rota de API

    Dicas de depuração:
    • Use console.log para exibir informações
    • Consulte os logs do Vercel Edge Functions
    • Capture erros com try-catch
  6. 6

    Step 6: Testar e depurar

    O que testar:
    • Todos os caminhos que devem corresponder
    • Caminhos fora do matcher, para garantir que não sejam interceptados
    • A lógica de redirecionamento
    • A compatibilidade com Edge Runtime

    Como depurar:
    • Adicione console.log ao middleware
    • Confira a aba Network do navegador
    • Consulte os logs do Vercel Edge Functions
    • Veja avisos no modo de desenvolvimento do Next.js

FAQ

O que fazer quando a configuração matcher do Middleware não funciona?
Confira:
1) se o caminho do matcher está correto e se rotas dinâmicas usam *
2) se os recursos estáticos foram excluídos
3) se o formato do caminho é válido para o Next.js, em vez de uma expressão regular incompatível

Adicione console.log ao middleware para ver quais caminhos estão correspondendo.
Por que caminhos com vários níveis não correspondem?
A rota dinâmica precisa usar `:path*`, com asterisco, para abranger vários níveis. Por exemplo, `/dashboard/:path*` corresponde a `/dashboard/settings/profile`, enquanto `/dashboard/:path` só corresponde a um nível, como `/dashboard/settings`.
O que fazer quando uma biblioteca não é compatível com Edge Runtime?
O Edge Runtime não é um ambiente Node.js completo e não aceita todos os pacotes npm.

Soluções:
1) Verifique se o pacote oferece suporte ao Edge Runtime
2) Substitua-o por APIs padrão da Web
3) Leve a lógica complexa para uma rota de API ou Server Component
4) Use uma biblioteca alternativa compatível com Edge Runtime
Como evitar um loop infinito de redirecionamento?
Garanta que o destino do redirecionamento fique fora do matcher. Se o destino for `/login`, por exemplo, exclua esse caminho do matcher. Uma opção é usar o negative lookahead `'/((?!login|api).*)'`.
É possível acessar o banco de dados no Middleware?
Não é recomendado. O Middleware roda no Edge Runtime e deve permanecer leve.

Se uma consulta ao banco for necessária:
1) chame uma rota de API pelo Middleware
2) use variáveis de ambiente para configurações
3) mova a lógica complexa para uma rota de API ou Server Component
Como depurar o Middleware?
Métodos:
1) adicione console.log à função middleware
2) confira requisições e respostas na aba Network do navegador
3) consulte os logs do Vercel Edge Functions
4) use o modo de desenvolvimento do Next.js para ver avisos e erros
Qual é a diferença entre Middleware e rotas de API?
Middleware:
• roda no Edge Runtime
• é executado antes de a requisição chegar à página ou rota de API
• é indicado para interceptação e encaminhamento leves

Rotas de API:
• rodam no runtime do Node.js
• podem usar todas as APIs do Node.js e acessar bancos de dados
• são indicadas para lógica de negócio complexa

26 min de leitura · Publicado em: 25 dez 2025 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog