Next.js: Migrando do Pages Router para App Router com Estratégia
Como migrar gradualmente aplicações Next.js existentes para App Router sem reescrever tudo, usando funcionalidades híbridas e transição incremental.
Resumo executivo
Como migrar gradualmente aplicações Next.js existentes para App Router sem reescrever tudo, usando funcionalidades híbridas e transição incremental.
Ultima atualizacao: 17/03/2026
O dilema da transição
O App Router do Next.js (introduzido no Next.js 13) oferece recursos poderosos: React Server Components, streaming, layouts aninhados e paralelismo de dados. Mas migrar uma aplicação Pages Router estabelecida é não-trivial. Códigos em _app.tsx, _document.tsx, getServerSideProps e getStaticProps não funcionam diretamente.
O erro comum é tentar "big bang migration" — migrar tudo de uma vez. Isso meses de desenvolvimento com risco alto. Uma abordagem melhor é migração incremental: ambos os routers coexistem enquanto você move funcionalidades gradualmente.
Arquitetura híbrida: os routers podem coexistir
Next.js 13+ permite Pages Router e App Router no mesmo projeto. Esta funcionalidade foi construída especificamente para migrações.
my-app/
├── app/ # App Router (rotas /app/*)
│ ├── layout.tsx
│ ├── page.tsx
│ └── dashboard/
│ └── layout.tsx
├── pages/ # Pages Router (rotas /pages/*)
│ ├── _app.tsx
│ ├── _document.tsx
│ └── legacy/
│ └── page.tsx # /legacy atende aqui
└── public/Regras de precedência:
- Rotas em
app/têm precedência sobrepages/ - Rotas
pages/só servem se não houver correspondência emapp/ - Ambos compartilham o mesmo
public/e configuração
Estratégia de migração por fases
Fase 1: Preparação e infraestrutura (1-2 semanas)
typescript// 1. Adiciona App Router mantendo Pages Router
// Cria app/layout.tsx sem quebrar pages/
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR">
<body>
{/* Header/Footer podem ser compartilhados */}
<Header />
{children}
<Footer />
</body>
</html>
);
}typescript// 2. Cria app/page.tsx (homepage nova)
// Testa que App Router funciona sem quebrar app/pages/index.tsx
export default function HomePage() {
return (
<div>
<h1>Homepage (App Router)</h1>
</div>
);
}typescript// 3. Configura compartilhamento de estilos
// app/layout.tsx importa estilos globais existentes
import '../styles/globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR">
<body>
{children}
</body>
</html>
);
}Fase 2: Migração de páginas públicas (2-4 semanas)
Páginas públicas (homepage, sobre, contato) são bons pontos de partida:
typescript// Antes: pages/index.tsx com getStaticProps
export default function Home({ posts }: { posts: Post[] }) {
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
export async function getStaticProps() {
const posts = await fetchPosts();
return { props: { posts } };
}typescript// Depois: app/page.tsx com Server Component
export default async function Home() {
const posts = await fetchPosts();
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}Benefício: server components eliminam necessidade de getStaticProps e simplificam código.
Fase 3: Migração de componentes compartilhados (3-6 semanas)
Componentes que funcionam em ambos os routers precisam de atenção especial:
typescript// Componente compatível com ambos os routers
// Usa Client Component apenas quando necessário
'use client'; // Apenas se precisar de hooks/interatividade
export function InteractiveButton({ onClick }: { onClick: () => void }) {
return (
<button onClick={onClick}>
Clique aqui
</button>
);
}typescript// Componente Server Component (padrão no App Router)
// Pode fazer fetch de dados diretamente
export async function PostList() {
const posts = await fetchPosts();
return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}**Heurística para use client:**
- Componentes com
useState,useEffect, event handlers →'use client' - Componentes apenas apresentacionais sem hooks → Server Component (padrão)
- Componentes com bibliotecas que só funcionam no client →
'use client'
Fase 4: Migração de rotas de negócio (4-8 semanas)
Rotas de negócio (dashboard, checkout, perfil) são mais complexas:
typescript// Antes: pages/dashboard/[id].tsx
export default function DashboardPage({ params }: { params: { id: string } }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/dashboard/${params.id}`)
.then(res => res.json())
.then(setData)
.finally(() => setLoading(false));
}, [params.id]);
if (loading) return <Spinner />;
return <DashboardView data={data} />;
}
export async function getServerSideProps({ params }: GetServerSidePropsContext) {
// Autenticação acontece aqui
const session = await getSession();
if (!session) {
return { redirect: { destination: '/login', permanent: false } };
}
return { props: {} };
}typescript// Depois: app/dashboard/[id]/page.tsx
export default async function DashboardPage({
params
}: {
params: { id: string };
}) {
// Fetch de dados direto no server
const data = await fetchDashboardData(params.id);
return <DashboardView data={data} />;
}
// app/dashboard/[id]/layout.tsx com middleware
import { auth } from '@/app/lib/auth';
export default async function DashboardLayout({
children,
params
}: {
children: React.ReactNode;
params: { id: string };
}) {
// Autenticação no layout
const session = await auth();
if (!session) {
redirect('/login');
}
return (
<DashboardShell>
{children}
</DashboardShell>
);
}Padrões de autenticação:
- Pages Router:
getServerSideProps→ middleware opcional - App Router: middleware ou layout → Server Component auth
Fase 5: Refatoração de API routes (2-4 semanas)
typescript// Antes: pages/api/users.ts
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const users = await getUsers();
res.json(users);
}typescript// Depois: app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const users = await getUsers();
return NextResponse.json(users);
}Benefício: API Routes do App Router usam Web Standard Request/Response, mais padronizado.
Padrões de transição específicos
Compartilhando estado entre routers
typescript// shared/store.ts funciona em ambos os routers
import { createStore } from 'zustand';
type Store = {
user: User | null;
setUser: (user: User) => void;
};
export const useStore = createStore<Store>((set) => ({
user: null,
setUser: (user) => set({ user }),
}));typescript// pages/_app.tsx (Pages Router)
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}typescript// app/layout.tsx (App Router)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR">
<body>
<StoreProvider>
{children}
</StoreProvider>
</body>
</html>
);
}Migrando layouts
typescript// Antes: pages/_app.tsx com layout global
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Header />
<main>
<Component {...pageProps} />
</main>
<Footer />
</>
);
}typescript// Depois: app/layout.tsx (layout raiz)
import Header from '@/components/Header';
import Footer from '@/components/Footer';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR">
<body>
<Header />
<main>
{children}
</main>
<Footer />
</body>
</html>
);
}
// app/dashboard/layout.tsx (layout aninhado)
import DashboardNav from '@/components/DashboardNav';
export default function DashboardLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<>
<DashboardNav />
<div className="dashboard-content">
{children}
</div>
</>
);
}Benefício: layouts aninhados permitem UI compartilhada por grupo de rotas sem repetição.
Anti-padrões de migração
Anti-padrão 1: 'use client' em tudo
typescript// RUIM: Todo arquivo tem 'use client'
'use client';
export function StaticContent() {
return <div>Conteúdo estático</div>;
}Solução:
typescript// BOM: Remove 'use client', usa Server Component
export function StaticContent() {
return <div>Conteúdo estático</div>;
}Anti-padrão 2: Copiar lógica de getServerSideProps
typescript// RUIM: Tenta replicar getServerSideProps
export default async function Page() {
const { searchParams } = props;
const data = await fetchData(searchParams);
return <View data={data} />;
}
// Tentando recriar getInitialProps
export async function generateMetadata() {
// Isso não é getInitialProps!
}Solução:
typescript// BOM: Usa Server Component com fetch direto
export default async function Page({
searchParams
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
const data = await fetchData(searchParams);
return <View data={data} />;
}Anti-padrão 3: Ignorar streaming de dados
typescript// RUIM: Busca tudo antes de renderizar
export default async function Page() {
const posts = await fetchPosts();
const comments = await fetchComments();
const analytics = await fetchAnalytics();
return (
<View
posts={posts}
comments={comments}
analytics={analytics}
/>
);
}Solução:
typescript// BOM: Usa paralelismo de fetch
import PostList from '@/components/PostList';
import CommentList from '@/components/CommentList';
import AnalyticsView from '@/components/AnalyticsView';
export default async function Page() {
const [posts, comments, analytics] = await Promise.all([
fetchPosts(),
fetchComments(),
fetchAnalytics()
]);
return (
<div>
<Suspense fallback={<PostListSkeleton />}>
<PostList posts={posts} />
</Suspense>
<Suspense fallback={<CommentListSkeleton />}>
<CommentList comments={comments} />
</Suspense>
<AnalyticsView analytics={analytics} />
</div>
);
}Benefício: streaming permite renderizar progressivamente enquanto dados carregam.
Testando migração gradualmente
Estratégia de rollout
typescript// Feature flag para controlar migração
const USE_APP_ROUTER = process.env.NEXT_PUBLIC_USE_APP_ROUTER === 'true';
export default function HomePage() {
if (USE_APP_ROUTER) {
// Usa componente App Router
return <AppRouterHomePage />;
}
// Usa componente Pages Router original
return <PagesRouterHomePage />;
}Monitoramento de bugs
typescript// A/B test entre routers
import { useFeatureFlag } from '@/hooks/useFeatureFlag';
export default function HomePage() {
const useAppRouter = useFeatureFlag('app-router-homepage');
if (useAppRouter) {
return <AppRouterHomePage />;
}
return <PagesRouterHomePage />;
}typescript// Track performance por router
export function withRouterTracking<P extends object>(
Component: React.ComponentType<P>,
routerType: 'app' | 'pages'
) {
return function TrackedComponent(props: P) {
useEffect(() => {
metrics.track('page_load', {
router_type: routerType,
page: typeof window !== 'undefined' ? window.location.pathname : 'unknown'
});
}, []);
return <Component {...props} />;
};
}Checklist de conclusão
Por funcionalidade migrada, verifique:
- [ ] Renderização igual visual e funcionalmente
- [ ] SEO (metadata, Open Graph) preservado
- [ ] Autenticação funcionando corretamente
- [ ] Performance não degradou (Core Web Vitals)
- [ ] Logs de erro não aumentaram
- [ ] Links internos funcionam
- [ ] Formulários submitem corretamente
Conclusão
Migrar do Pages Router para App Router não precisa ser um evento traumático de big bang. A arquitetura híbrida do Next.js permite transição incremental: novos recursos em App Router, código legado em Pages Router, ambos coexistindo enquanto você migra funcionalidade por funcionalidade.
A estratégia certa depende da complexidade da aplicação e da tolerância a risco de mudança. Aproache incremental reduz risco técnico: você pode testar cada migração isoladamente, fazer rollback se necessário e aprender com problemas antes de migrar código crítico.
Mais importante que a velocidade de migração é a qualidade da transição. Páginas públicas primeiro, componentes compartilhados depois, rotas de negócio por último. Cada fase adiciona complexidade — certifique-se que a anterior está estável antes de avançar.
O resultado final vale o esforço: Server Components, layouts aninhados, streaming de dados e paralelismo de fetch tornam o aplicativo mais rápido e eficiente. A migração é investimento em performance e desenvolvedor experience que se paga ao longo do tempo.
Precisa migrar sua aplicação Next.js para App Router com previsibilidade técnica? Fale com especialistas da Imperialis em React e Next.js para desenhar e executar uma estratégia de migração incremental segura.