Next.js 16: Guia Completo de Migração de middleware.ts para proxy.ts

O Next.js 16 trocou middleware.ts por proxy.ts, migrando do Edge Runtime para Node.js. Aprenda a migrar com codemod, integrar Auth.js e next-intl, e evitar os erros mais comuns.

Introdução: Por Que o middleware.ts Virou proxy.ts no Next.js 16

Se você trabalha com Next.js há algum tempo, provavelmente já tem um arquivo middleware.ts no seu projeto. Talvez ele faça verificação de cookies, talvez cuide de redirecionamentos por geolocalização, ou talvez proteja rotas que só usuários logados deveriam acessar. Pois bem — com o Next.js 16, esse arquivo ganhou um nome novo. E, mais importante, um runtime completamente diferente.

A mudança de middleware.ts para proxy.ts não é só estética. A equipe do Next.js percebeu que o termo "middleware" confundia muita gente — especialmente quem vinha do Express.js e tratava o arquivo como lugar pra enfiar lógica de negócios pesada. O nome "proxy" deixa a intenção muito mais clara: é uma camada de rede na frente da aplicação, feita pra redirecionamentos, rewrites e verificações leves. Nada de consultas ao banco ou validações complexas de JWT aqui.

Mas a mudança que realmente importa tá por baixo do capô.

O proxy.ts roda no runtime Node.js por padrão, e não mais no Edge Runtime. Na prática, isso significa acesso completo às APIs nativas do Node.js, compatibilidade total com pacotes npm, e uma mudança fundamental na forma como a gente pensa sobre interceptação de requisições no Next.js. Honestamente, era uma mudança que já deveria ter acontecido antes.

Neste guia, vou cobrir tudo que você precisa saber: o porquê da mudança, como migrar passo a passo (com e sem codemod), como adaptar integrações com Auth.js e next-intl, padrões práticos, e os erros mais comuns que aparecem durante a migração.

O Que Mudou: middleware.ts vs proxy.ts

Antes de meter a mão na massa, vale entender exatamente o que mudou — e o que ficou igual.

Comparação Direta

Aspectomiddleware.ts (Antigo)proxy.ts (Next.js 16+)
StatusDeprecado (ainda funciona com aviso)Atual e recomendado
Runtime padrãoEdge RuntimeNode.js Runtime
APIs Node.jsNão disponíveisAcesso completo
Pacotes npmApenas compatíveis com EdgeTodos suportados
Conexão com bancoNão possívelTecnicamente possível (mas não recomendado)
Distribuição globalSim (edge nodes)Não (região única)
Cold startMuito rápidoPadrão serverless
Função exportadamiddleware()proxy()

Por Que a Mudança de Runtime Importa

O Edge Runtime original foi escolhido pro middleware por ser absurdamente rápido e rodar globalmente, perto dos usuários. O problema? Ele é basicamente um sandbox com APIs limitadas — fetch e Web Crypto API, e olhe lá. Isso criava dores de cabeça reais: bibliotecas que dependiam de APIs Node.js simplesmente não funcionavam, não tinha acesso ao filesystem, e ORMs como Prisma precisavam de adaptadores especiais pra funcionar.

A vulnerabilidade CVE-2025-29927 acabou acelerando tudo. Ela mostrou que autenticação baseada exclusivamente em middleware no Edge Runtime podia ser contornada sob carga alta. A resposta da Vercel no Next.js 16 foi direta: os limites de rede precisam ser explícitos, e o proxy roda em Node.js pra garantir estabilidade.

Importante: O middleware.ts antigo ainda funciona, mas exibe um aviso de deprecação. Ele vai ser removido numa versão futura do Next.js. Se você precisa continuar no Edge Runtime especificamente, pode manter o middleware.ts por enquanto — mas a recomendação oficial é migrar.

Migração Automática com Codemod

A forma mais rápida (e segura) de migrar é usando o codemod oficial. Ele faz o trabalho pesado: renomeia o arquivo, atualiza o nome da função exportada e ajusta as flags de configuração.

Executando o Codemod

# Migração completa para Next.js 16 (inclui middleware → proxy)
npx @next/codemod upgrade 16

# Ou, se quiser rodar apenas a migração de middleware para proxy
npx @next/codemod@canary middleware-to-proxy .

O codemod cuida automaticamente de:

  • Renomear middleware.ts (ou .js) para proxy.ts
  • Renomear a exportação middleware para proxy
  • Trocar experimental.middlewarePrefetch por experimental.proxyPrefetch
  • Trocar experimental.middlewareClientMaxBodySize por experimental.proxyClientMaxBodySize
  • Trocar experimental.externalMiddlewareRewritesResolve por experimental.externalProxyRewritesResolve
  • Trocar skipMiddlewareUrlNormalize por skipProxyUrlNormalize

Verificando a Migração

Depois de rodar o codemod, vale dar uma conferida pra ter certeza que tudo ficou certo:

# Confirma que o arquivo foi renomeado
ls -la src/proxy.ts  # ou proxy.ts na raiz

# Testa localmente
npm run dev

# Procura por referências antigas que o codemod pode ter perdido
grep -r "middleware" --include="*.ts" --include="*.tsx" --include="*.js" .

Migração Manual Passo a Passo

Se você prefere ter controle total sobre a migração — ou se o codemod não cobre alguma configuração customizada do seu projeto — dá pra fazer tudo manualmente. O processo é simples, mas exige atenção.

Passo 1: Renomear o Arquivo

# Se o arquivo está na raiz do projeto
mv middleware.ts proxy.ts

# Se o arquivo está dentro de src/
mv src/middleware.ts src/proxy.ts

Passo 2: Renomear a Função Exportada

Antes (middleware.ts):

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const sessao = request.cookies.get('session_token')

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

  return NextResponse.next()
}

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

Depois (proxy.ts):

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  const sessao = request.cookies.get('session_token')

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

  return NextResponse.next()
}

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

Perceba que a única mudança no código é o nome da função: de middleware para proxy. O matcher, os imports, a lógica toda — tudo continua igual. Simples assim.

Passo 3: Atualizar Configurações no next.config.ts

Se você usa flags de configuração que referenciam "middleware", elas também precisam mudar:

// next.config.ts — ANTES
const nextConfig = {
  skipMiddlewareUrlNormalize: true,
  experimental: {
    middlewarePrefetch: 'strict',
  },
}

// next.config.ts — DEPOIS
const nextConfig = {
  skipProxyUrlNormalize: true,
  experimental: {
    proxyPrefetch: 'strict',
  },
}

Passo 4: Testar Localmente

# Inicie o servidor de desenvolvimento
npm run dev

# Teste as rotas protegidas
# Verifique se os redirecionamentos funcionam
# Confira se não há warnings de deprecação no console

Padrões Práticos com proxy.ts

Beleza, você já entende a migração. Agora vamos ao que interessa: como aproveitar o proxy.ts com padrões que cobrem os cenários mais comuns em apps Next.js de produção.

Verificação Leve de Autenticação

Esse é o padrão mais usado: verificar se o usuário tem um cookie de sessão e redirecionar pra login se não tiver. O ponto-chave aqui é manter o proxy leve — nada de validações complexas nessa camada.

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const rotasPublicas = new Set(['/', '/login', '/cadastro', '/sobre'])

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

  // Não intercepta rotas públicas
  if (rotasPublicas.has(pathname)) {
    return NextResponse.next()
  }

  // Verifica existência do cookie de sessão
  const tokenSessao = request.cookies.get('session_token')

  if (!tokenSessao) {
    const urlLogin = new URL('/login', request.url)
    urlLogin.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(urlLogin)
  }

  return NextResponse.next()
}

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

Esse padrão funciona bem porque só verifica a existência do cookie, sem decodificar nem validar o token. A validação de verdade deve acontecer nos Server Components ou Server Actions, onde você tem acesso ao banco de dados.

Redirecionamento por Geolocalização

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const mapaLocales: Record<string, string> = {
  BR: 'pt-BR',
  PT: 'pt-PT',
  US: 'en',
  GB: 'en',
  ES: 'es',
  FR: 'fr',
}

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

  // Ignora se já tem locale no path
  const temLocale = /^\/(pt-BR|pt-PT|en|es|fr)(\/|$)/.test(pathname)
  if (temLocale) {
    return NextResponse.next()
  }

  // Detecta país e mapeia para locale
  const pais = request.geo?.country || 'US'
  const locale = mapaLocales[pais] || 'en'

  return NextResponse.redirect(
    new URL(`/${locale}${pathname}`, request.url)
  )
}

Teste A/B com Rewrite

Esse é um padrão que eu pessoalmente acho muito elegante. A ideia é usar rewrites pra servir variantes diferentes da mesma página sem que o usuário perceba a mudança na URL.

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

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

  if (pathname === '/precos') {
    // Verifica se o usuário já foi atribuído a um grupo
    const grupo = request.cookies.get('ab_precos')?.value

    if (grupo === 'B') {
      return NextResponse.rewrite(new URL('/precos/variante-b', request.url))
    }

    if (!grupo) {
      // Atribui aleatoriamente ao grupo A ou B
      const novoGrupo = Math.random() < 0.5 ? 'A' : 'B'
      const resposta = novoGrupo === 'B'
        ? NextResponse.rewrite(new URL('/precos/variante-b', request.url))
        : NextResponse.next()

      resposta.cookies.set('ab_precos', novoGrupo, {
        maxAge: 60 * 60 * 24 * 30, // 30 dias
        httpOnly: true,
      })

      return resposta
    }
  }

  return NextResponse.next()
}

Adicionando Headers de Segurança

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  const resposta = NextResponse.next()

  // Headers de segurança para todas as páginas
  resposta.headers.set('X-Frame-Options', 'DENY')
  resposta.headers.set('X-Content-Type-Options', 'nosniff')
  resposta.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  resposta.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()'
  )

  return resposta
}

Organizando o Proxy de Forma Modular

Em projetos reais, o proxy acaba acumulando várias responsabilidades: autenticação, i18n, headers de segurança, testes A/B... Jogar tudo num arquivo só vira bagunça rápido. A boa notícia é que, mesmo o Next.js suportando apenas um arquivo proxy.ts por projeto, dá pra organizar a lógica em módulos separados.

// lib/proxy/autenticacao.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function verificarAutenticacao(request: NextRequest) {
  const rotasProtegidas = ['/dashboard', '/configuracoes', '/perfil']
  const { pathname } = request.nextUrl

  const ehProtegida = rotasProtegidas.some(rota =>
    pathname.startsWith(rota)
  )

  if (!ehProtegida) return null

  const token = request.cookies.get('session_token')
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return null // Continua normalmente
}
// lib/proxy/headers-seguranca.ts
import { NextResponse } from 'next/server'

export function aplicarHeadersSeguranca(resposta: NextResponse) {
  resposta.headers.set('X-Frame-Options', 'DENY')
  resposta.headers.set('X-Content-Type-Options', 'nosniff')
  return resposta
}
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verificarAutenticacao } from '@/lib/proxy/autenticacao'
import { aplicarHeadersSeguranca } from '@/lib/proxy/headers-seguranca'

export function proxy(request: NextRequest) {
  // 1. Verificação de autenticação
  const respostaAuth = verificarAutenticacao(request)
  if (respostaAuth) return respostaAuth

  // 2. Aplica headers de segurança
  const resposta = NextResponse.next()
  aplicarHeadersSeguranca(resposta)

  return resposta
}

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

Essa abordagem mantém o proxy.ts enxuto e cada módulo focado numa única responsabilidade. Conforme o projeto cresce, você vai adicionando novos módulos sem poluir o arquivo principal.

Integrando Auth.js (NextAuth v5) com proxy.ts

Essa é, sem dúvida, a parte que mais causa confusão durante a migração. Se você usava o padrão export { auth as middleware }, essa sintaxe simplesmente não funciona no proxy.ts — o Next.js espera uma função chamada proxy, não uma re-exportação.

O Problema

Com middleware.ts, muitos projetos faziam assim:

// middleware.ts (ANTIGO - não funciona em proxy.ts)
export { auth as middleware } from '@/auth'

Ao migrar pra proxy.ts, esse padrão gera um erro bem pouco amigável do Turbopack:

Error: Proxy is missing expected export.
The file proxy.ts must export a function, either as a default export
or as a named proxy export.

A Solução

Crie uma função proxy explícita que chama o auth por dentro:

// auth.config.ts — Configuração separada (sem adapter de banco)
import type { NextAuthConfig } from 'next-auth'

export const authConfig: NextAuthConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const estaLogado = !!auth?.user
      const ehDashboard = nextUrl.pathname.startsWith('/dashboard')

      if (ehDashboard) {
        if (estaLogado) return true
        return false // Redireciona para login
      }

      return true
    },
  },
  providers: [], // Providers ficam no auth.ts principal
}
// proxy.ts
import NextAuth from 'next-auth'
import { authConfig } from '@/auth.config'
import type { NextRequest } from 'next/server'

const { auth } = NextAuth(authConfig)

export async function proxy(request: NextRequest) {
  // @ts-expect-error — auth retorna NextResponse internamente
  return auth(request)
}

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

O segredo aqui é separar a configuração do Auth.js em dois arquivos: auth.config.ts (sem adapter de banco, leve o suficiente pro proxy) e auth.ts (com adapter completo, usado nos Server Components e Server Actions). Isso evita que o proxy tente carregar dependências pesadas de banco de dados — algo que, na minha experiência, é o erro número um de quem faz essa integração pela primeira vez.

Integrando next-intl com proxy.ts

Se seu projeto usa next-intl pra internacionalização, a migração é bem tranquila. O next-intl já suporta oficialmente o proxy.ts, então não tem surpresa.

Configuração Básica

// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing'

export const routing = defineRouting({
  locales: ['pt', 'en', 'es'],
  defaultLocale: 'pt',
})
// src/proxy.ts
import createMiddleware from 'next-intl/middleware'
import { routing } from './i18n/routing'

export default createMiddleware(routing)

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

Combinando next-intl com Autenticação

Quando você precisa de internacionalização e autenticação no mesmo proxy, a ordem das operações faz diferença:

// src/proxy.ts
import createMiddleware from 'next-intl/middleware'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { routing } from './i18n/routing'

const handleI18n = createMiddleware(routing)

export default async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  // 1. Primeiro: internacionalização (para todas as rotas)
  const respostaI18n = handleI18n(request)

  // 2. Depois: verifica autenticação para rotas protegidas
  const rotasProtegidas = ['/dashboard', '/configuracoes']
  const ehProtegida = rotasProtegidas.some(rota =>
    pathname.includes(rota)
  )

  if (ehProtegida) {
    const token = request.cookies.get('session_token')
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  return respostaI18n
}

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

Erros Comuns na Migração e Como Resolver

Depois de acompanhar bastante discussão no GitHub e em comunidades de devs, juntei os erros mais frequentes que aparecem durante a migração. Se você tá travado em algum deles, provavelmente vai encontrar a solução aqui.

Erro 1: "Proxy is missing expected export"

Causa: O arquivo proxy.ts existe mas não exporta uma função com o nome correto.

// ERRADO — re-exportação não funciona
export { auth as proxy } from '@/auth' // Pode falhar

// CORRETO — função explícita
export async function proxy(request: NextRequest) {
  // sua lógica aqui
  return NextResponse.next()
}

Erro 2: Loop Infinito de Redirecionamento

Causa: O proxy redireciona pra /login, mas /login também é interceptado pelo proxy, gerando um ciclo infinito. Clássico.

// ERRADO
export function proxy(request: NextRequest) {
  const token = request.cookies.get('session_token')
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

// CORRETO — exclui a página de login
export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  // Não intercepta rotas públicas
  if (pathname === '/login' || pathname === '/cadastro') {
    return NextResponse.next()
  }

  const token = request.cookies.get('session_token')
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

Erro 3: Assets Estáticos Sendo Interceptados

Causa: O matcher não exclui arquivos estáticos, e aí CSS, JS e imagens param de carregar. Já vi isso acontecer mais vezes do que gostaria de admitir.

// CORRETO — matcher que exclui assets estáticos
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Erro 4: Bibliotecas Incompatíveis com o Runtime

Causa: Se você ainda usa middleware.ts (Edge Runtime), bibliotecas Node.js vão falhar. Se já migrou pro proxy.ts (Node.js), bibliotecas que dependiam de APIs Web específicas podem dar problema.

A boa notícia? Com o proxy.ts no Node.js, agora você pode usar APIs nativas:

// Com proxy.ts no Node.js runtime, isso funciona!
import { createHash } from 'crypto'

export function proxy(request: NextRequest) {
  const apiKey = request.headers.get('x-api-key')
  if (apiKey) {
    const hash = createHash('sha256').update(apiKey).digest('hex')
    // Verificação usando API nativa do Node.js
  }
  return NextResponse.next()
}

Boas Práticas: O Que Fazer e O Que Evitar no proxy.ts

Com a mudança pro runtime Node.js, fica tentador transformar o proxy.ts num mini backend. Não caia nessa. Sério. O proxy é uma camada de rede, não um servidor de aplicação.

Faça

  • Verificações leves de cookies — checar se um cookie de sessão existe (sem decodificar)
  • Redirecionamentos programáticos — baseados em geolocalização, user-agent ou parâmetros de URL
  • Rewrites pra testes A/B — servir variantes diferentes da mesma página
  • Headers de segurança — CSP, X-Frame-Options, CORS
  • Rate limiting básico — baseado em headers ou IP (com parcimônia)

Evite

  • Consultas ao banco de dados — mesmo sendo possível agora, bloqueia o TTFB de toda requisição
  • Validação completa de JWT — faça isso nos Server Components ou Server Actions
  • Lógica de autorização granular — verificar permissões específicas deve ficar perto dos dados
  • Chamadas a APIs externas lentas — qualquer operação bloqueante afeta todas as requisições
  • Gerenciamento de sessão completo — o proxy é pra controle de tráfego, não pra gestão de sessão

Regra de ouro: Se a operação adiciona mais de 10-20ms de latência, ela não deveria estar no proxy. Mova pra Server Components, Server Actions ou Route Handlers.

Checklist de Migração Completa

Antes de considerar a migração concluída, passe por cada item desta lista:

  1. Renomear middleware.ts para proxy.ts (ou usar o codemod)
  2. Renomear a função exportada de middleware para proxy
  3. Atualizar flags de configuração no next.config.ts (skipMiddlewareUrlNormalize → skipProxyUrlNormalize, etc.)
  4. Atualizar integrações de autenticação (Auth.js, Clerk, Supabase Auth)
  5. Atualizar integrações de i18n (next-intl)
  6. Verificar compatibilidade de bibliotecas com o runtime Node.js
  7. Testar todas as rotas protegidas localmente
  8. Testar redirecionamentos e rewrites
  9. Verificar que assets estáticos não são interceptados
  10. Rodar grep -r "middleware" --include="*.ts" pra encontrar referências perdidas
  11. Fazer deploy em staging antes de ir pra produção

Perguntas Frequentes (FAQ)

O middleware.ts vai parar de funcionar?

Não agora. O middleware.ts ainda funciona no Next.js 16, mas exibe um aviso de deprecação. Vai ser removido numa versão futura. Se você depende do Edge Runtime, pode manter ele por enquanto — a equipe do Next.js prometeu documentação adicional sobre Edge Runtime numa versão minor futura.

Posso usar proxy.ts no Edge Runtime?

Não. O proxy.ts roda exclusivamente no runtime Node.js, e isso não é configurável. Se você precisa de latência global mínima com Edge Runtime, continue usando middleware.ts enquanto ele ainda é suportado.

O proxy.ts afeta a performance do meu site?

Pode afetar, sim — se não for usado direito. Como o proxy roda pra toda requisição que bate com o matcher, qualquer operação lenta (consultas ao banco, chamadas a APIs externas) vai aumentar o TTFB de todas as páginas. Mantenha a lógica leve e use o matcher pra limitar o escopo.

Como faço autenticação segura no Next.js 16?

A recomendação é uma abordagem de defesa em profundidade: use o proxy.ts pra verificações leves (existência de cookie), Server Components pra validação de sessão, e Server Actions pra mutações autenticadas. Nunca dependa exclusivamente do proxy pra segurança — a CVE-2025-29927 provou que isso é arriscado.

O codemod funciona com projetos que usam src/?

Funciona, sim. O codemod detecta automaticamente se seu projeto usa a estrutura com diretório src/ e renomeia o arquivo na localização certa. Ele também atualiza imports relativos que referenciam o arquivo antigo.

Sobre o Autor Editorial Team

Our team of expert writers and editors.