Nuxt is Vue with the decisions already made: file-based routing, server rendering, data fetching and a server directory come built in, so you go from an empty folder to a deployed full-stack app with npx nuxi init, a pages directory, useFetch and one build command.
If you already write Vue single-file components, most of Nuxt is subtraction. You stop hand-wiring vue-router, stop choosing a bundler config, and stop inventing a place for your API. What is left is a handful of conventions worth learning in order, and one decision that matters more than the rest: whether your pages render on the server per request or once at build time. This tutorial walks the whole path with commands you can paste, and it is honest about the traps, because most of the Nuxt pain people post about comes from the deploy step and not the code.
Table of contents
- Scaffold the project with nuxi init
- Pages and routing from the file system
- Layouts, components and shared state
- Fetching data with useFetch and building server routes
- SSR versus SSG, and what nuxi generate does
- Deploying the app you just built
- How this fits the rest of the stack
- FAQ
Scaffold the project with nuxi init
Start with the official scaffolder. You need a current Node LTS release, and the command creates the folder, installs dependencies and gives you a working dev server in about a minute.
npx nuxi@latest init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev
The dev server runs on port 3000 with hot module replacement. Open it and you see the welcome screen, which is rendered by app.vue. That file is the root of the application. Replace its contents with a single NuxtPage component and Nuxt will start routing for you.
A note on folders, because the layout moved between major versions. Nuxt 4 puts application code under an app directory, so you will see app/app.vue, app/pages and app/layouts, while server code stays in a server directory at the project root. Nuxt 3 keeps pages and layouts at the root next to nuxt.config.ts. The conventions are identical, only the parent folder differs, so if a tutorial shows a different path than your project, that is the reason and not a mistake on your part.
<!-- app/app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
Everything after this is convention. Drop a file in the right folder and it becomes a route, a layout, a composable or an API endpoint, with imports handled automatically. That auto-import behaviour is the thing that feels like magic on day one and like a debugging headache on day thirty, so learn early that the generated type files in the hidden nuxt folder show you exactly what was imported.
Pages and routing from the file system
Any Vue file inside the pages directory becomes a route. pages/index.vue is the home page, pages/about.vue is /about, and a bracketed name makes a dynamic segment. This replaces the router config you would otherwise maintain by hand, and it is the feature most Vue developers stop missing vue-router after about ten minutes.
pages/
index.vue -> /
about.vue -> /about
posts/
index.vue -> /posts
[slug].vue -> /posts/hello-world
[...all].vue -> catch-all 404
Read the dynamic segment with the useRoute composable, and link between pages with NuxtLink rather than a plain anchor so navigation stays client-side after the first load.
// pages/posts/[slug].vue, inside the setup block
const route = useRoute()
const slug = route.params.slug
<template>
<main>
<h1>Post: {{ slug }}</h1>
<NuxtLink to="/posts">Back to all posts</NuxtLink>
</main>
</template>
Nested routes work the same way. A file named posts.vue next to a posts folder becomes a parent route that renders its children where you place a NuxtPage. Use that for account areas with a shared sidebar. Use definePageMeta inside a page for per-route settings such as which layout to use, or a middleware that guards the route. Keep route middleware small; a redirect to a login page is a good use, business logic is not.
Layouts, components and shared state
A layout is a wrapper component with a slot. Put a file at layouts/default.vue and every page uses it unless it asks for something else. This is where the header, footer and navigation live, so you write them once.
<!-- layouts/default.vue -->
<template>
<div>
<header><NuxtLink to="/">Home</NuxtLink></header>
<slot />
<footer>Built with Nuxt</footer>
</div>
</template>
For a page that needs a different shell, such as a stripped-down checkout or a dashboard, add layouts/dashboard.vue and opt in with definePageMeta({ layout: ‘dashboard’ }). Anything inside the components directory is auto-imported by filename, so a file called PostCard.vue is usable as PostCard anywhere with no import line.
Shared state is where beginners reach for a store too early. Nuxt gives you useState, a small composable that creates reactive state keyed by name and, importantly, serialises it from the server to the client so the two agree after hydration. Reach for Pinia when you have real cross-page state with actions and devtools needs. For a theme toggle or a logged-in user, useState is enough and one dependency lighter.
Fetching data with useFetch and building server routes
Data fetching is the part of Nuxt that most differs from a plain Vue app. useFetch runs on the server during the first render, hands the result to the client in the page payload, and skips the duplicate request on hydration. Calling fetch inside onMounted, the habit from single-page apps, throws that benefit away and gives crawlers an empty page.
// pages/posts/index.vue, inside the setup block
const { data: posts, status, error, refresh } = await useFetch('/api/posts')
<template>
<p v-if="status === 'pending'">Loading...</p>
<p v-else-if="error">Could not load posts.</p>
<ul v-else>
<li v-for="p in posts" :key="p.id">{{ p.title }}</li>
</ul>
</template>
That request hits a route you write yourself. Files under server/api become endpoints served by Nitro, the server engine underneath Nuxt. Put the HTTP method in the filename if you want to restrict it.
// server/api/posts.get.ts
export default defineEventHandler(() => {
return [
{ id: 1, title: 'Hello Nuxt' },
{ id: 2, title: 'Server routes are just files' }
]
})
Return a plain object and Nitro serialises it as JSON. Read query strings with getQuery(event), route params with getRouterParam, and request bodies with readBody. Secrets belong here, in runtimeConfig on the server side, never in code that ships to the browser. Use useFetch for data a page needs to render, and useAsyncData when you want to wrap something that is not a plain URL, such as a call to an SDK. Use $fetch for event handlers like form submits, where you do not want render-time caching behaviour.
SSR versus SSG, and what nuxi generate does
This is the decision that shapes your hosting. With server-side rendering, the default, a running Node process builds the HTML for each request. With static site generation, Nuxt renders every route once at build time and writes plain files, and nothing runs on a server afterwards.
# SSR: build a Node server, then run it
npm run build
node .output/server/index.mjs
# SSG: render every route to static HTML
npx nuxi generate
# output lands in .output/public
SSG is the right call for a marketing site, docs or a blog whose content changes when you deploy. It is cheap, fast and cannot fall over under load because there is nothing to fall over. SSR is the right call when the first response depends on who is asking or on data that changes by the minute: a logged-in dashboard, a store with live stock, personalised pages.
You do not have to pick one for the whole app. Route rules let you mix modes in nuxt.config.ts, prerendering the pages that never change and leaving the rest server-rendered. A hybrid app still needs a running server, because the dynamic routes have to be answered somewhere.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/blog/**': { prerender: true },
'/dashboard/**': { ssr: true }
}
})
Two traps worth knowing. Dynamic routes in a fully static build are only generated if the crawler can discover a link to them, so add explicit paths to the prerender routes list when a page is reachable only by search. And setting ssr to false turns the app into a client-rendered single-page app, which is a legitimate choice for an admin tool and a poor one for anything that needs to rank.
Deploying the app you just built
Deployment follows directly from the mode. A pure nuxi generate build is a folder of static files, so it belongs on a static host with the publish directory set to .output/public. Anything using SSR, server routes at runtime or hybrid route rules is a long-running Node process, so it belongs on a service that keeps a process alive, restarts it when it crashes and passes it a port.
The practical checklist for the Node case is short. The build command is npm run build. The start command is node .output/server/index.mjs. Nitro reads the PORT environment variable, so the host and the app agree on where to listen. Put secrets in environment variables and read them through runtimeConfig, not through committed files. Most failed first deploys come down to one of those three lines being wrong, not to Nuxt.
If you want the full comparison of the rendering modes against hosts, the Nuxt hosting guide covers it in depth, and the official Nuxt deployment documentation lists the presets for each platform. Once you have something running, the Nuxt portfolio walkthrough is a good second project to practise routing and content on.
How this fits the rest of the stack
A tutorial app costs nothing to run and a real one does not cost much more, but the bill depends on the rendering mode you chose two sections ago. A static generate build is files on a CDN. An SSR build is a small Node service, and an app with a database is that service plus a Postgres or MySQL instance. Before you commit to either, put the numbers side by side in the RunxBuild hosting calculator, which shows the service, database and bandwidth as separate line items. When you are ready, connect the repository from the RunxBuild dashboard, set the build and start commands from this tutorial, and read the Node services documentation for how the PORT variable and logs work. Rollback to the previous deploy is there if the first release misbehaves, which for a first Nuxt deploy is a reasonable thing to plan for.
Useful related references:
- Nuxt hosting for SSG, SSR and hybrid apps
- Building a Nuxt portfolio site
- Deploying a Vue app on RunxBuild, step by step
- Node services on RunxBuild
FAQ
Is Nuxt worth learning if I already know Vue?
Yes, if you need SEO, fast first paint or a backend in the same repository. Nuxt adds file-based routing, server rendering, data fetching and a server directory on top of the Vue you already know. If you are building a client-only tool behind a login, plain Vue with Vite is simpler and there is little to gain.
What is the difference between Nuxt and Vue?
Vue is the UI library: components, reactivity and templates. Nuxt is a framework built on Vue that adds routing from your folder structure, server-side rendering, static generation, auto-imports, data-fetching composables and a Nitro server for API routes. You write Vue components in both; Nuxt decides how they are routed, rendered and shipped.
Should I use SSR or SSG in Nuxt?
Use SSG when your content changes only when you redeploy, such as blogs, docs and marketing pages. Use SSR when the response depends on the user or on frequently changing data. You can mix both with routeRules, prerendering stable pages and rendering the rest on demand from one server.
What does npx nuxi generate do?
It renders every discoverable route to static HTML at build time and writes the result to .output/public. There is no server afterwards, so any static host can serve it. Dynamic routes are only included if the crawler finds links to them or you list them explicitly in the prerender routes configuration.
How do I fetch data in Nuxt?
Use useFetch for a URL and useAsyncData for anything else, both inside the setup block. They run on the server during the first render and pass the result to the client, avoiding a duplicate request on hydration. Use $fetch for actions triggered by the user, like submitting a form.