Two routers, one repo — for now
Next.js still ships both routers because legacy apps haven't all migrated. New work belongs in the App Router (app/ directory). The Pages Router (pages/) exists for migration and won't see new feature work.
| Capability | Pages Router | App Router |
|---|---|---|
| Directory | pages/ | app/ |
| Default render target | Client | Server |
| Data fetching | getServerSideProps / getStaticProps | async components, direct fetch |
| Layouts | Manual _app.tsx wrap | layout.tsx per segment |
| Loading UI | Manual | loading.tsx + Suspense |
| Error UI | _error.tsx | error.tsx + global-error.tsx |
| Streaming | Limited | First-class |
| Server Actions | No | Yes |
The mental model shift
Pages Router thinks in pages with separate data-fetching functions. App Router thinks in composable Server & Client Components organized by route segments. Most "Next.js feels weird now" reactions are about this shift, not about syntax.
Coexistence rules
Both routers work in the same project, segment by segment. If a path exists in both app/ and pages/, App Router wins. Migrate route by route; you don't need a big-bang rewrite.
Use App Router as the default for new routes and migrate an existing Pages Router application one route at a time. Move each data read, permission check, layout lifetime, loading state, and error boundary to the component or segment that owns it. Renaming pages/ to app/ is not a migration. Carrying getServerSideProps thinking into a client-marked page preserves the old responsibility split while adding new framework boundaries and bundle cost.
Take one route and classify every line of its old data function as read, authorization, transformation, or view input. Mark the new owner in the App Router tree, visit both direct and navigated URLs, and confirm the old route no longer competes for the same path.