Vercel Logo

Set Up the Hot Springs Finder

Every framework has a shelving system. In Next.js, everything goes on one bookcase: pages, layouts, API routes, loading states, error boundaries, all in the app/ directory. You know where to reach because you've memorized the system.

Nuxt shelves things differently. Client code on one shelf, server code on another. Layouts get their own section. Components auto-import themselves.

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

A few things that might trip you up:

  • 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 are aggressive. Components in components/ (including subfolders), composables in the top level of composables/ (subfolders are not auto-imported), and all Vue utilities are available everywhere without importing them. This feels wrong at first. It's not.

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";

That's it. No tailwind.config.js, no postcss.config.js. If you've been maintaining those files in Next.js projects, this will feel suspiciously easy.

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. You'll land on /springs with a placeholder message. That's expected. We haven't wired up data fetching yet.

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>