Vercel Logo

Set Up the Hot Springs Finder

In Next.js, pages, layouts, API routes, loading states, and error boundaries all live under app/.

Nuxt separates client code from server code, gives layouts their own directory, and auto-imports components.

Outcome

Scaffold a Nuxt 4 app with Tailwind CSS and understand how the project structure maps to Next.js.

Fast Track

  1. Clone the starter repo and install dependencies
  2. Open the project and compare the file structure to Next.js
  3. Start the dev server and confirm the home page loads

Hands-on exercise 1.1

Clone the starter repo and get oriented in the Nuxt project structure.

Requirements:

  1. Clone the starter repo and install dependencies with pnpm install
  2. Review the file structure, paying attention to app/, server/, and nuxt.config.ts
  3. Start the dev server with pnpm dev and visit http://localhost:3000
  4. Read through nuxt.config.ts and identify what each option does

Implementation hints:

  • The app/ directory is Nuxt 4's way of separating your client-side code from server code. In Next.js, everything lives under app/ too, but in Nuxt, server/ is a completely separate directory with its own auto-imports
  • nuxt.config.ts is the equivalent of next.config.js, but with a lot more built in. No need for separate Tailwind config files with v4
  • Nuxt auto-imports Vue utilities (ref, computed, watch) and its own composables (useFetch, useRoute). No import statements needed for these

Here's how the project structure maps to what you already know:

Nuxt 4                          Next.js
─────                           ───────
app/                            app/
  pages/                          (route files)
  components/                     components/
  layouts/                        (layout.tsx files)
  composables/                    hooks/
  assets/                        assets/
/public                         public/ or styles/
server/                         app/api/
  api/                            (route handlers)
  routes/                         (no equivalent)
  data/                           (no equivalent)
nuxt.config.ts                  next.config.js

Pay attention to these differences:

  • server/ is not inside app/. In Next.js, your API routes live alongside your pages. In Nuxt, the server is a separate world powered by Nitro. It has its own auto-imports, its own utilities, and it doesn't know about Vue.
  • No layout.tsx files inside page folders. Nuxt uses a dedicated layouts/ directory. As long as app.vue wraps <NuxtPage /> in <NuxtLayout>, default.vue is applied to every page automatically.
  • Auto-imports cover several categories. Components in components/ (including subfolders), top-level composables in composables/, and Vue utilities are available without explicit imports. Composables in nested folders are not auto-imported.

Let's look at the config file that holds it all together:

nuxt.config.ts
import tailwindcss from "@tailwindcss/vite";
 
export default defineNuxtConfig({
  compatibilityDate: "2025-05-01",
 
  css: ["~/assets/css/main.css"],
 
  vite: {
    plugins: [tailwindcss()],
  },
});

Note that the ~ in the CSS path is an alias for app.

Nuxt 4 uses the app/ directory for client code and server/ for server code by default. If you've seen older Nuxt 3 projects with everything at the project root, that's the old way. Nuxt 4 enforces the separation out of the box.

The compatibilityDate tells Nuxt which version of breaking-change behavior to use. Think of it like a snapshot: any breaking changes introduced after this date won't affect your app until you update the date.

Tailwind v4 doesn't need a config file. The @tailwindcss/vite plugin handles everything, and main.css just imports it:

app/assets/css/main.css
@import "tailwindcss";

You do not need tailwind.config.js or postcss.config.js for this setup.

Try It

Start the dev server:

pnpm dev

Visit http://localhost:3000. You should see the home page with "Find your next soak" and a "Browse Hot Springs" button.

Click "Browse" in the nav. The /springs page still shows its placeholder because data fetching comes later.

If pnpm dev fails

Make sure you ran pnpm install first. If you see a Vue version mismatch error, delete node_modules and pnpm-lock.yaml, then run pnpm install again. Nuxt 4 requires Vue 3.5+.

Port already in use?

If port 3000 is taken, Nuxt will automatically try 3001. Check your terminal output for the actual URL.

Commit

git init && git add -A && git commit -m "feat(setup): scaffold Nuxt 4 app with Tailwind"

Done-When

  • pnpm dev starts without errors
  • Home page loads at http://localhost:3000 with the Hot Springs Finder heading
  • You can navigate to /springs and see the placeholder page
  • You can explain why Nuxt 4 separates server/ from app/

Solution

The starter repo already contains the complete setup for this lesson. Your project structure should look like this:

hot-springs-finder/
├── app/
│   ├── app.vue
│   ├── assets/css/main.css
│   ├── components/
│   ├── composables/
│   ├── layouts/
│   │   └── default.vue
│   └── pages/
│       ├── index.vue
│       ├── favorites.vue
│       ├── visited.vue
│       ├── login.vue
│       └── springs/
│           ├── index.vue
│           └── [id].vue
├── server/
│   ├── api/
│   └── data/
│       └── springs.json
├── public/
├── nuxt.config.ts
├── package.json
└── tsconfig.json
app/app.vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
app/pages/index.vue
<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>

Was this helpful?

supported.