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

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ística | Edge Runtime | Node.js Runtime |
|---|---|---|
| Inicialização | Cold start próximo de zero | Pode levar centenas de milissegundos |
| Onde roda | Pontos de presença globais | Servidor específico |
| APIs disponíveis | APIs padrão da Web | APIs completas do Node.js |
| Uso indicado | Lógica leve e resposta rápida | Cá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 camposrequest.cookies: leitura e escrita mais simples de cookiesrequest.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:
| Problema | Forma errada | Forma correta | Motivo |
|---|---|---|---|
| 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 fora | matcher: ['/dashboard/:path*'] | matcher: ['/', '/dashboard/:path*'] | / não é incluído automaticamente |
| Recursos estáticos são interceptados | matcher: ['/:path*'] | matcher: ['/((?!_next|favicon.ico).*)'] | É preciso excluir caminhos internos como _next |
| Rotas de API não ficam protegidas | matcher: ['/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?
| Categoria | API ou módulo incompatível | Impacto |
|---|---|---|
| Sistema de arquivos | fs, path | Não é possível ler ou gravar arquivos locais |
| Subprocessos | child_process | Não é possível executar comandos externos |
| Criptografia | Algumas APIs de crypto | É preciso usar Web Crypto API |
| Banco de dados | Drivers nativos de MongoDB e MySQL | A maioria dos drivers tradicionais não funciona |
| Outros | process.emit, setImmediate | Algumas APIs de baixo nível do Node.js não existem |
Qual é o impacto prático?
Os efeitos mais diretos são:
- Você não pode consultar diretamente o banco para validar a identidade do usuário.
- Você não pode ler arquivos de configuração, como um
config.json. - 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 identidade | Não consulta o banco | Validar JWT localmente ou chamar uma rota de API |
| Criptografar e descriptografar | Parte de crypto não funciona | Usar Web Crypto API |
| Ler configurações | Sem acesso ao sistema de arquivos | Usar variáveis de ambiente (process.env) ou uma API |
| Registrar logs | Não grava arquivos locais | Enviar dados a um serviço de logs, como Logtail |
| Operar no banco | Drivers tradicionais não funcionam | Usar 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
joseem vez dejsonwebtoken, pois o segundo depende do módulocryptodo Node.js. - Leia o segredo do JWT por uma variável de ambiente;
process.envfunciona 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
joseem vez dejsonwebtoken - 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:
- O parâmetro
fromregistra o destino original e permite voltar após o login. - 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.
- 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_SECRETnã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:
- Há três níveis de detecção: parâmetro da URL > cookie > configuração do navegador.
- O cookie preserva a escolha e a reaplica na próxima visita.
- 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:
- Use
rewrite, nãoredirect, para que o usuário continue vendo/na barra de endereço. - O cookie mantém o grupo estável e evita mudanças de versão ao atualizar a página.
- 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:
| Causa | Como verificar | Solução |
|---|---|---|
| Arquivo no local errado | Confirme que middleware.ts está na raiz ou em src | Mova o arquivo para o local correto |
| Matcher não cobre o caminho | Registre request.nextUrl.pathname | Ajuste a configuração do matcher |
| Erro de sintaxe | Confira erros de compilação no console | Corrija a sintaxe |
| Função exportada incorretamente | Confirme o uso de export function middleware | Corrija a exportação |
| Cache antigo | Limpe o diretório .next | Rode 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
fsepath jsonwebtoken, que deve ser substituído porjose- drivers nativos de MongoDB e MySQL, que precisam de alternativas compatíveis com Edge
Soluções:
- Procure uma alternativa: consulte a documentação para saber se há uma versão compatível com Edge Runtime.
- 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.
- Use APIs padrão da Web: substitua
cryptodo 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:
- O arquivo
.env.localnão existe ou o nome da variável está incorreto. - A variável não foi configurada na plataforma de implantação, como Vercel ou Netlify.
- 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
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
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
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
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
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
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?
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?
O que fazer quando uma biblioteca não é compatível com Edge Runtime?
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?
É possível acessar o banco de dados no Middleware?
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?
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?
• 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
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Tutorial de Server Actions no Next.js: boas práticas para formulários e validação
Aprenda na prática a processar formulários com Server Actions no Next.js, validar dados com Zod, aplicar medidas de segurança e melhorar a experiência do usuário.
Parte 10 de 51
Próximo
Proteção de rotas e controle de acesso no Next.js: guia completo de Middleware e defesa em camadas
Entenda como proteger rotas e controlar permissões no Next.js, do Middleware à defesa em camadas, usando NextAuth e getServerSession para implementar um sistema RBAC seguro, com exemplos completos de código.
Parte 12 de 51



Comentários
Entre com GitHub para comentar