Pages & Routing
In Next.js, a route is a file called page.tsx inside a folder that matches the URL. Want /about? Create app/about/page.tsx. Want /springs/[id]? Create app/springs/[id]/page.tsx. The folder is the route, the file is always page.tsx.
Nuxt flips this. The file IS the route. Want /about? Create app/pages/about.vue. No wrapper folder, no mandatory filename. The trade-off is subtle but it matters: Next.js routes are verbose but explicit, Nuxt routes are compact but rely on you knowing the naming rules.
Let's build out the pages for Hot Springs Finder and see where those rules match up and where they diverge.
Outcome
Create the route structure for the Hot Springs Finder with home, browse, and detail pages.
Fast Track
- Create page files in
app/pages/for each route - Add
NuxtLinknavigation between pages - Verify routes resolve correctly in the browser
Hands-on exercise 1.2
Build the page structure for the app. Right now, the starter has placeholder pages. We'll update them with real structure and connect them with navigation.
Requirements:
- Verify these pages exist in
app/pages/and understand which URL each one maps to - Update the home page with a link to the browse page using
NuxtLink - Navigate between pages and confirm the routes work
Implementation hints:
NuxtLinkis the equivalent of Next.js'sLinkcomponent. It's auto-imported, so you don't need to import it- In Nuxt,
pages/springs/index.vuemaps to/springsandpages/springs/[id].vuemaps to/springs/:id. In Next.js, those would beapp/springs/page.tsxandapp/springs/[id]/page.tsx - Square brackets for dynamic segments work the same way conceptually, but the file structure is flatter
Here's the side-by-side for every route in our app:
URL Nuxt file Next.js file
─── ───────── ────────────
/ pages/index.vue app/page.tsx
/springs pages/springs/index.vue app/springs/page.tsx
/springs/:id pages/springs/[id].vue app/springs/[id]/page.tsx
/favorites pages/favorites.vue app/favorites/page.tsx
/visited pages/visited.vue app/visited/page.tsx
/login pages/login.vue app/login/page.tsx
Notice the pattern. Next.js always needs a folder. Nuxt lets you use either a folder with index.vue or a standalone file. /favorites doesn't need a favorites/ folder because there are no nested routes under it. /springs uses a folder because it has a child route ([id]).
Now let's look at how navigation works. In Next.js, you'd write:
import Link from "next/link";
<Link href="/springs">Browse</Link>In Nuxt, NuxtLink is auto-imported. It accepts href, but to is the convention across Vue Router and most Vue UI libraries. We'll use to:
<NuxtLink to="/springs">Browse</NuxtLink>The home page already uses this pattern. Let's look at the link that takes users from the home page to the browse page:
<template>
<div>
<section>
<h1>
Find your next soak
</h1>
<p>
Discover hot springs around the world. Browse by region, temperature, or
type. Save your favorites and track the ones you've visited.
</p>
<div>
<NuxtLink to="/springs">
Browse Hot Springs
</NuxtLink>
</div>
</section>
</div>
</template>NuxtLink handles client-side navigation automatically. No full page reload, no manual prefetching. When a NuxtLink enters the viewport, Nuxt prefetches the target route's code in the background. You get the performance behavior for free.
One thing that might catch you off guard: useRoute() in Nuxt returns a reactive route object, similar to Next.js's useParams() and useSearchParams() combined into one. We'll use this heavily when we build the detail page in a later lesson.
<script setup lang="ts">
const route = useRoute();
// route.params.id — same as useParams() in Next.js
// route.query.search — same as useSearchParams().get("search")
</script>No import needed. useRoute is auto-imported like everything else in Nuxt.
useRoute() gives you the current route (read-only). For programmatic navigation — what Next.js's useRouter().push() does — Nuxt prefers navigateTo("/springs"), which works on the server and handles redirects cleanly. useRouter() exists if you need the raw router instance, but reach for navigateTo first.
In Next.js, a catch-all route is [...slug]/page.tsx. In Nuxt, it's [...slug].vue. Same concept, different syntax. We won't need one in this project, but it's a common gotcha when translating patterns.
Try It
Start the dev server if it's not already running:
pnpm dev- Visit
http://localhost:3000. Click "Browse Hot Springs." You should land on/springs - Check the URL bar. Click the browser back button. You should return to
/without a full page reload - Visit
http://localhost:3000/springs/breitenbush-hot-springsdirectly. You should see the detail page placeholder - Visit
http://localhost:3000/favoritesandhttp://localhost:3000/visited. Both should render their placeholder content
All six routes should resolve without 404 errors.
Commit
git add -A && git commit -m "feat(routing): verify page routes and NuxtLink navigation"Done-When
- All six routes (
/,/springs,/springs/:id,/favorites,/visited,/login) render without errors - Clicking
NuxtLinknavigates without full page reloads - You can explain when to use
useRoute(),navigateTo(), anduseRouter()in Nuxt - You can map any Next.js route file to its Nuxt equivalent
Solution
The starter already contains all the page files. The key files for routing are:
app/pages/
├── index.vue → /
├── favorites.vue → /favorites
├── visited.vue → /visited
├── login.vue → /login
└── springs/
├── index.vue → /springs
└── [id].vue → /springs/:id
<template>
<div>
<section>
<h1>
Find your next soak
</h1>
<p>
Discover hot springs around the world. Browse by region, temperature, or
type. Save your favorites and track the ones you've visited.
</p>
<div>
<NuxtLink to="/springs">
Browse Hot Springs
</NuxtLink>
</div>
</section>
</div>
</template><template>
<div>
<h1>
Browse Hot Springs
</h1>
<p>
Hot springs will be listed here once we wire up data fetching.
</p>
</div>
</template><template>
<div>
<h1>
Spring Detail
</h1>
<p>
Individual spring details will appear here once we build dynamic routes.
</p>
</div>
</template>Was this helpful?