Understanding Nuxt.js vs. Vue.js
Nuxt.jsis an open-source framework built on top of Vue.js. While Vue.js is a library focused on the view layer (the UI), Nuxt is a "meta-framework" that provides the architecture, directory structure, and configurations needed to build production-ready applications.
Think of it this way: Vue provides the engine, while Nuxt provides the entire car.
Key Differences at a Glance
The following table highlights the architectural and functional shifts between the two:
| Feature | Vue.js | Nuxt.js |
|---|---|---|
| Category | Frontend Library / Core Framework | Full-stack Meta-framework |
| Rendering | Primarily Client-Side Rendering (CSR) | SSR, Static Site Generation (SSG), and CSR |
| Routing | Manual configuration via vue-router |
File-system based (automatic) |
| SEO | Challenging (requires extra setup) | Excellent (built-in SSR/Static support) |
| State Management | Manual setup (Pinia/Vuex) | Auto-imported and pre-configured |
| Folder Structure | Flexible / Unopinionated | Strict / Opinionated (convention over configuration) |
Core Enhancements in Nuxt.js
- Rendering Modes: Nuxt allows you to choose how your app is delivered. It supports Rendering (SSR), which sends fully rendered HTML to the browser, and Site Generation (SSG), which pre-builds the site at deployment.
-
Automatic Routing: You don't need to write a router file. If you
create a file named
about.vuein thepages/directory, Nuxt automatically creates the/aboutroute for you. - SEO & Meta Tags: Nuxt provides built-in components and composables
(like
useHead) to easily manage meta titles, descriptions, and scripts for individual pages, making it superior for search engine visibility. - Auto-imports: Nuxt automatically imports components, composables, and Vue APIs, reducing boilerplate code and keeping your script tags clean.
- Nitro Engine: Nuxt uses the Nitro server engine, allowing you to write API routes (server-side code) directly within the same project.
In the context of Nuxt, "Convention over Configuration" is a design philosophy that minimizes the number of decisions a developer needs to make. Instead of manually configuring every aspect of the application (as you would in a raw Vue.js setup), Nuxt assumes "sensible defaults" based on how you name and place your files.
If you follow the established conventions (the folder structure), Nuxt handles the configuration (the underlying code) automatically.
How Convention Replaces Configuration
The following table compares the manual effort required in a standard Vue project versus the automated "convention" approach in Nuxt:
| Task | Manual Configuration (Standard Vue) | Nuxt Convention (Automated) |
|---|---|---|
| Routing |
Import vue-router and manually map paths
to components in a router.js file.
|
Create a .vue file in the /pages
directory; the route is generated based on the filename.
|
| State Management | Install Pinia, create a store, and manually import it into components. |
Place a file in the /stores directory;
Nuxt auto-imports it for use anywhere.
|
| Components |
Import every component manually:
import MyButton from './components/MyButton.vue'.
|
Place components in the /components
directory; they are globally available without imports.
|
| Meta Tags / SEO |
Use a library like vue-meta and configure
it for every single route.
|
Use the useHead composable or the
app.config.ts for automated SEO handling.
|
| API / Server Code | Set up a separate Express/Node server and handle CORS and proxying. |
Add files to the /server/api directory;
Nuxt handles the Nitro server setup automatically.
|
Key Benefits of This Approach
- Faster Development (Speed to Market): You spend less time writing "glue code" (boilerplate) and more time writing feature logic.
- Standardization: Because the structure is opinionated, any Nuxt developer can jump into a project and immediately know where the routes, stores, and components are located.
- Reduced Decision Fatigue: You don't have to argue over where to put files or how to name them; the framework has already decided the most efficient way.
- Built-in Optimization: Because Nuxt knows your structure, it can automatically perform code-splitting—only loading the JavaScript necessary for the specific page the user is visiting.
Rendering Modes Comparison
Nuxt 3 is unique because it allows you to mix and match different rendering strategies within a single application using Hybrid Rendering. By default, it uses Universal Rendering, but you can configure specific routes to behave differently.
Rendering Modes Comparison
The table below breaks down the primary ways Nuxt handles content delivery:
| Rendering Type | When It Renders | Best Use Cases | Benefits |
|---|---|---|---|
| Universal (SSR) | On every request (Server-side) | E-commerce, dynamic dashboards | High SEO + Live data |
| Static (SSG) | During the build step | Blogs, documentation, marketing sites | Fastest performance + Cheap hosting |
| Client-Side (CSR) | In the browser (Client-side) | Internal tools, SaaS (post-login) | Low server load |
| SWR (Stale-While-Revalidate) | On-demand (then cached) | Product pages, social feeds | Fast cached delivery + Auto-updates |
| ISR (Incremental Static) | On-demand (cached for set time) | High-traffic news sites | Balance of static speed and dynamic content |
Detailed Breakdown
- The server generates the HTML and sends it to the browser.
- Once the browser receives the HTML, it "hydrates" it (downloads JS to make it interactive).
- Pros:Excellent SEO and fast "First Contentful Paint." Client-Side Rendering (CSR):
- You can disable SSR by setting
ssr: falseinnuxt.config.ts - The browser receives a nearly empty HTML file and builds the UI using JavaScript.
- Pros: Ideal for highly interactive apps where SEO is not a priority (e.g., an admin panel). Hybrid Rendering:
Universal Rendering (Default):
This allows you to define Route Rules in your config file. For example, you can make your homepage static but keep your search results dynamic.
To start a new Nuxt 3 project, you use nuxi, the Nuxt Command Line Interface. It is designed to be lightweight and handles the scaffolding of the project structure automatically.
Quick Start Command
Run the following command in your terminal to initialize a project:
npx nuxi@latest init
Step-by-Step Installation Process
| Step | Action | Command / Note |
|---|---|---|
| 1. Initialize | Run the init command. | npx nuxi@latest init my-app |
| 2. Select Package Manager | Choose npm, yarn, or pnpm. | Use arrow keys to select your preference. |
| 3. Install Dependencies | The CLI will ask to install packages. | Select Yes to run npm install
automatically. |
| 4. Navigate | Enter your new project folder. | cd my-app |
| 5. Start Development | Launch the local dev server. |
npm run dev -- -o
(The -o opens your browser).
|
Key Prerequisites
- Node.js: Ensure you have version 18.10.0 or newer installed.
- Text Editor: Visual Studio Code is recommended, specifically with the Volar extension (configured for Vue 3).
- Browser: Any modern evergreen browser (Chrome, Firefox, Edge, Safari).
What Happens Behind the Scenes?
When you run the init command, Nuxt:
- Clones a minimalstarter template.
-
Sets up the directory structure(e.g.,
.nuxt,server,public). - Pre-configures TypeScriptsupport by default.
- Prepares the Nitro serverengine for development.
The nuxt.config.ts file is the central configuration hub for a Nuxt 3 application. It allows you to override the default "Convention over Configuration" settings, register modules, and define global behaviors for both the client and the server.Because it is written in TypeScript, it provides full IntelliSense (autocompletion) to help you discover available options as you type
Core Responsibilities of nuxt.config.ts
| Category | Purpose | Example Usage |
|---|---|---|
| App Head | Global SEO and Metadata | Setting the default <title>, meta tags, and external
scripts. |
| Modules | Extending Functionality | Registering tools like @nuxtjs/tailwindcss,
@pinia/nuxt, or @nuxt/content.
|
| Route Rules | Rendering Strategies | Defining which pages are Static (SSG), Server-rendered (SSR), or Cached (SWR). |
| Runtime Config | Environment Variables | Managing private keys (server-side only) and public keys (client-side). |
| CSS/Plugins | Global Assets | Importing global CSS files or registering custom Nuxt plugins. |
| Nitro Server | Backend Config | Configuring the underlying server engine for deployments (e.g., Vercel, Netlify). |
Key Features and Structure
-
defineNuxtConfigWrapper:This helper function ensures your configuration is type-checked. - Runtime Config: Nuxt distinguishes between public keys (accessible in the browser) and private keys (accessible only by the Nitro server), preventing sensitive data leaks.
- Auto-updates: In development mode, Nuxt watches this file; most changes will trigger an automatic restart of the development server to apply the new settings.
Code Example: A Typical Config
In Nuxt, the In Nuxt, the means your application's URL structure is
defined by the physical files and folders within the pages/ directory. Nuxt
uses the Nitro engine to scan this directory and automatically generate a router
configuration using vue-router under the hood.
This removes the need to manually maintain a router.js file.
Mapping Files to URLs
The naming convention of your files directly determines the path. Here is how common routing patterns are translated:
| File Path | Resulting URL Route | Type of Route |
|---|---|---|
pages/index.vue |
/ |
Home Page |
pages/about.vue |
/about |
Static Route |
pages/users/index.vue |
/users |
Nested Directory Route |
pages/users/[id].vue |
/users/123, /users/abc |
Dynamic Route |
pages/blog/[...slug].vue |
/blog/my/long/path |
Catch-all Route |
pages/404.vue |
/404 |
Custom Error Page |
Key Routing Concepts
-
Dynamic Routes(
[id].vue): Use square brackets to define a parameter. You can access this in your component usinguseRoute().params.id. -
Catch-all Routes (
[...slug].vue): The ellipsis allows the route to match multiple path segments (e.g.,/blog/2026/02/news). -
Nested Routes: If you create a folder and a
.vuefile with the same name (e.g.,parent.vueandparent/child.vue) , Nuxt will treat the child as a nested view inside acomponent within the parent. - Navigation: Instead of standard
atags, Nuxt provides thecomponent. This enables smart pre-fetching (loading the next page's data before the user even clicks) and faster client-side transitions. - Mount Service: Handles SD cards and internal storage mounting.
- Telephony Registry: Monitors signal strength and call status.
Validation and Middleware
You can also add a definePageMeta macro inside your page files to:
- Add Middleware (e.g., checking if a user is logged in before showing the page).
- Set a specific Layout
- Validate route parameters (e.g., ensuring an ID is a number).
The .nuxt directory is the generated output of the Nuxt
development environment. It serves as the "brain" of your project while it is running in
development mode or being prepared for production.
You should generally never manually edit files inside this folder, as they are
overwritten every time you run npm run dev or npm run build.
Key Functions of the .nuxt Directory
| Function | Description |
|---|---|
| Code Generation | Converts your Vue files, layouts, and components into the final JavaScript that the browser and server understand. |
| Route Mapping | Stores the generated routes.js file created by the
file-based routing system. |
| Auto-Imports | Maintains the "virtual" files that allow you to use components and
composables without explicit import statements. |
| Type Definitions | Generates TypeScript declaration files (.d.ts) so your IDE
(like VS Code) provides accurate autocompletion. |
| App Entry Point | Contains the main client-side and server-side entry points that initialize the Vue application. |
Crucial Facts for Developers
- Git Ignore:This directory is automatically added to your
.gitignore. You should not commit it to version control because its contents are specific to the local machine and the current build. - Virtual File System: Nuxt uses a virtual file system (VFS) to
bridge the gap between your source code and the running application. The
.nuxtfolder is the physical manifestation of that VFS. - Troubleshooting: If you encounter strange "module not found" errors
or TypeScript mismatches, a common fix is to delete the
.nuxtfolder and restart your development server. This forces Nuxt to regenerate everything from scratch. - Types Generation: You can manually trigger the generation of these files (specifically for IDE support) by running:
In Nuxt, dynamic routes are created using square bracket syntax [] within
your file or folder names in the pages/ directory. This tells Nuxt that the
segment of the URL is a variable (parameter) rather than a static string.
Creating the File Structure
To create a route like /user/123, you would structure your directory as
follows:
-
File Path:
pages/user/[id].vue -
Resulting URL:
/user/:id(where:idcan be anything like123,abcorjohn-doe).
Accessing the Route Parameter
Once the file is created, you need to extract the id to use it in your logic
(e.g., fetching user data from an API). You do this using the useRoute()
composable.
| Approach | Code Example |
|---|---|
Inside <script setup> |
const route = useRoute();console.log(route.params.id);
|
Inside <template> |
{{ $route.params.id }} |
Advanced Dynamic Routing Patterns
Nuxt supports more complex scenarios beyond simple IDs:
- Multiple Parameters:
pages/posts/[category]/[id].vuemaps to/posts/tech/123 - Catch-all Routes:
pages/docs/[...slug].vuemaps to/docs/intro. - Optional Parameters:
pages/user/[[id]].vue(double brackets) makes theidoptional, meaning it matches both/userand/user/123.
Parameter Validation
You can use the definePageMeta macro to ensure the dynamic parameter meets
specific criteria (e.g., ensuring an ID is a number). If validation fails, Nuxt will
automatically return a 404 error.
While both directories contain .vue files, they serve distinct
architectural purposes in a Nuxt application. The primary difference lies in
routing and lifecycle.
Core Differences
The following table summarizes the structural and functional divergence between the two:
| Feature | pages/ |
components/ |
|---|---|---|
| Primary Role | Defines the application's Routes (URLs). | Defines Reusable UI elements. |
| Routing | Automatically generates a URL (e.g., about.vue →
/about).
|
No URL; must be imported or used inside a page. |
| Auto-import | Handled by the Router. | Handled by Nuxt’s auto-import system. |
| Data Fetching | Typically where top-level API calls happen. | Receives data via props from pages. |
| Special Macros | Supports definePageMeta (middleware, layouts). |
Does not support page-level metadata macros. |
Detailed Roles
The pages/ Directory Directory
- Entry Points: Every file here is an entry point for a user.
- Orchestration: Pages act as "containers." They fetch data (using
useFetchoruseAsyncData) and then pass that data down to various components. - Layouts & Middleware: This is where you define which layout a page uses or which middleware should run before the user views the content.
The components/ components/
- Modularity: This is for "bricks" like
AppButton.vue,TheNavbar.vue, orUserProfileCard.vue. - Organized Subfolders: If you have
components/base/Button.vue, Nuxt allows you to use it asautomatically. - Lazy Loading: You can prefix a component with
Lazy(e.g.,) to delay downloading its code until it’s actually needed on the screen.
Visualizing the Relationship
- A Page is a specific destination (The "Profile" Page).
- A Component is a piece of that destination (The "Avatar" image or the "Follow" button).
Nuxt features a powerful Auto-import system that eliminates the need to
manually write import statements for your internal files or the Vue/Nuxt
APIs.
How Auto-imports Work
Nuxt scans specific directories during the development process and generates a Virtual
File System (stored in .nuxt/imports.d.ts) ) that makes these functions and
components globally available to your project.
| Directory / Source | What is Auto-imported? | Example Usage |
|---|---|---|
components/ |
All .vue files in this folder. |
<MyButton /> |
composables/ |
Named or default exports from files. | const { data } = useMyApi() |
utils/ |
Helper functions and logic. | formatDate(new Date()) |
| Vue & Nuxt APIs | Core functions like ref, computed,
useRoute.
|
const count = ref(0) |
Specific Rules for Components
-
Nested Folders: If you have
components/base/Button.vue, the component name is derived from the path:. -
Lazy Loading: You can skip the initial JavaScript bundle for a
component by prefixing it with
Lazy. For example, usingwill only download the component's code when the modal is actually rendered. - Direct Imports: If you prefer manual imports (e.g., for complex IDE refactoring), you can still import them normally; Nuxt will not conflict with manual imports.
Specific Rules for Composables
-
Top-level only Nuxt only scans the top-level files in the
composables/directory (e.g.,composables/useAuth.ts). -
Nested Composables: If you want to organize composables in
subfolders, you must either export them from an
index.tsin the root of the folder or configure thenuxt.config.tsto scan those subfolders.
Developer Experience (IDE Support)
s
Because Nuxt generates TypeScript declaration files in the .nuxt folder,
your IDE (like VS Code with Volar) will still provide:
- Autocompletion: Suggestions as you type the component or function name.
- Type Checking:Warnings if you pass the wrong props to an auto-imported component.
- Go to Definition: Clicking the component name will still take you to the source file.s
Universal Rendering (SSR) in Nuxt
Universal Rendering (also known as Server-Side Rendering or SSR) is a technique where the server executes the JavaScript code and generates the full HTML of a page before sending it to the browser.
Once the browser receives the HTML, it displays it immediately. It then downloads the JavaScript to "hydrate" the page, making it interactive (connecting event listeners, state, etc.).
Universal Rendering vs. Client-Side Rendering
| Feature | Universal Rendering (SSR) | Client-Side Rendering (CSR) |
|---|---|---|
| First Contentful Paint | Fast: User sees the page content almost instantly. | Slow: User sees a blank screen while JS downloads. |
| SEO | Excellent: Search engine crawlers see full HTML content. | Challenging: Crawlers may struggle to execute JS. |
| Server Load | Higher: Server must process every request. | Lower: Server just sends static files. |
| Initial Bundle Size | Smaller initial HTML payload. | Large initial JS bundle required to start. |
Why is it the Default?
Nuxt chooses Universal Rendering as the default because it provides the best balance of user experience and technical performance for modern web applications.
- Optimized SEO: Since the server sends a fully rendered page, search engines (Google, Bing) and social media bots (Twitter, Open Graph) can index your content accurately without needing to execute complex JavaScript.
- Perceived Performance: Users on slower devices or unstable mobile connections see the content much faster. They don't have to wait for a heavy JavaScript framework to initialize before reading the text.
- The "Best of Both Worlds": Nuxt doesn't just give you a static page. After the initial fast load, the app becomes a Page Application (SPA). This means subsequent navigations are nearly instantaneous because only the necessary data is fetched, not the whole page.
The Hydration Process
This multi-step process ensures that users see content immediately while still receiving a fully interactive experience:
- Request: The user hits the URL in their browser.
- Server: Nuxt renders the Vue components into full HTML strings on the server side.
- Response: The browser receives the pre-rendered HTML and displays the UI immediately (First Contentful Paint).
- Hydration: The browser downloads the Vue.js bundle and "takes over" the static HTML, turning it into a live, reactive application that can respond to user clicks and state changes.
To switch a Nuxt application to Single Page Application (SPA) mode, you effectively disable Server-Side Rendering (SSR) In this mode, the server sends an empty HTML shell with a tag, and the browser handles all the rendering, just like a standard Vue.js CLI or Vite project.
Method 1: Global SPA Mode
If you want the entire application to behave as an SPA, modify your
nuxt.config.ts file by setting the ssr property to
false.
- The server will no longer execute your Vue code.
- The application will be purely client-side.
- SEO will be limited as crawlers will see a mostly empty HTML file initially.
Method 2: Hybrid Rendering (Per-Route SPA)
Nuxt 3 allows you to keep the benefits of SSR for most of your site while making
specific sections (like an admin dashboard) behave as an SPA. This is done via
routeRules.
| Use Case | Configuration Example |
|---|---|
| Entire Site as SPA | ssr: false in root config. |
| Specific Folder (e.g., /admin) | routeRules: { '/admin/**': { ssr: false } } |
| Single Page (e.g., /dashboard) | routeRules: { '/dashboard': { ssr: false } } |
Key Considerations when Switching to SPA
Before disabling Universal Rendering, keep these technical shifts in mind to ensure your application functions as expected:
-
The
<ClientOnly>Component: In SPA mode, you don't strictly need this component, but it remains useful in SSR mode to wrap parts of a page that should only run in the browser (like a complex chart or a map). -
Data Fetching: In SPA mode,
useFetchanduseAsyncDatawill only run on the client. You won't see the "server-side" network call in the Nuxt logs; it will appear in the browser's Network tab instead. -
Deployment: When running
npm run buildfor a global SPA, Nuxt will generate a static entry point (usuallyindex.html) that can be hosted on any static web server (S3, Vercel, Netlify) without needing a Node.js environment.
Static Site Generation (SSG) is a rendering mode where Nuxt pre-renders your entire application into static HTML, CSS, and JavaScript files during the build process Instead of generating pages on-demand when a user visits (like SSR), the work is done upfront.
The resulting files are "flat" and can be hosted on any static web server or Content Delivery Network (CDN) without requiring a Node.js server.
How SSG Works in Nuxt
Static Site Generation (SSG) combines the SEO benefits of SSR with the low-cost hosting of static files:
-
Build Phase: When you run
nuxi generate, Nuxt crawls your routes and executes the logic for every page. -
Payload Generation: For every route (e.g.,
/about), Nuxt creates anindex.htmlfile and a small JSON "payload" containing the data fetched during the build. -
Deployment: You upload the
.output/publicfolder to a host like Netlify, Vercel, or GitHub Pages. - Client-Side Navigation: Once the first page loads, the app "hydrates" into a Single Page Application (SPA), making subsequent clicks instant.
SSG vs. Other Rendering Modes
| Feature | Static Site Generation (SSG) | Server-Side Rendering (SSR) |
|---|---|---|
| Server Required | No (Static Hosting) | Yes (Node.js Environment) |
| Build Time | Longer (proportional to page count) | Fast |
| Page Load Speed | Fastest (served from CDN edge) | Fast (depends on server speed) |
| Data Freshness | At time of build | Real-time |
| Best For | Blogs, Documentation, Portfolios | Dashboards, E-commerce, Social Media |
Key Nuxt Commands for SSG
To effectively generate a static site, you need to use the specific build commands and understand how Nuxt discovers your routes:
-
npx nuxi generate: This is the primary command for SSG. It builds the application and then exports it to static files. -
Dynamic Routes in SSG: For routes like
/blog/[slug], Nuxt needs to know which slugs exist to generate the pages. You can provide these innuxt.config.tsor let Nuxt crawl your<NuxtLink>tags to find them automatically.
The "Hybrid" Advantage
In Nuxt 3, you aren't forced to choose SSG for the whole site. Using
Route Rules, you can make your landing pages static (SSG) for speed, while
keeping your account settings page as a Single Page App (CSR).
Route Rules are the engine behind Hybrid Rendering in Nuxt 3. They allow you to define different caching and rendering strategies for different sections of your application within a single configuration file.
Instead of choosing one rendering mode for your entire app, you can assign rules to
specific URL patterns (e.g.,/blog/** vs. /admin/**).
Core Rendering Strategies in Route Rules
The following table explains the different "behaviors" you can apply to routes:
| Feature | Static Site Generation (SSG) | Server-Side Rendering (SSR) |
|---|---|---|
| Server Required | No (Static Hosting) | Yes (Node.js Environment) |
| Build Time | Longer (proportional to page count) | Fast |
| Page Load Speed | Fastest (served from CDN edge) | Fast (depends on server speed) |
| Data Freshness | At time of build | Real-time |
| Best For | Blogs, Documentation, Portfolios | Dashboards, E-commerce, Social Media |
How it Works: The Nitro Engine
Route Rules are processed by Nitro, Nuxt’s server engine. When a request comes in, Nitro checks the defined rules and decides whether to:
- Serve a pre-generated static file: Fast delivery from disk or CDN.
- Execute a server-side function: Renders the page fresh for every request (SSR).
- Serve a "stale" (cached) version: Delivers cached content while fetching new data in the background (SWR/ISR).
Example Configuration
In your nuxt.config.ts, you can mix these strategies to optimize performance
and SEO:
Why use Route Rules?
Route Rules provide the flexibility to optimize different parts of your application based on their specific requirements:
- Granular Control: You don't sacrifice SEO on your landing pages just because your dashboard needs to be an SPA.
- Cost Efficiency: SWR and ISR reduce server load by serving cached versions of dynamic pages.
- Global Deployment: Nitro optimizes these rules for the platform you are deploying to (Vercel, Netlify, Cloudflare, etc.).
Stale-While-Revalidate (SWR) is a caching strategy that allows Nuxt to serve a page instantly from a cache (the "stale" version) while simultaneously triggering a background re-generation of that page (the "revalidate" step) to ensure the next visitor sees updated content.
It provides the speed of a static site with the flexibility of a dynamic site.
The SWR Lifecycle
When a user requests a route configured with SWR, the following process occurs:
| Step | Action | Result |
|---|---|---|
| 1. Initial Request | User hits a URL for the first time. | The server renders the page (SSR) and stores it in the cache. |
| 2. Subsequent Request | Another user hits the same URL. | The server instantly serves the cached HTML (even if it's old). |
| 3. Background Refresh | Nuxt detects the cache is "stale." | Nitro triggers a background re-render to update the cache. |
| 4. Future Request | A later user hits the URL. | They receive the newly updated version from the previous background refresh. |
Configuring SWR in Nuxt
You enable SWR using routeRules in your nuxt.config.ts. You can
set it to true (infinite cache until a server restart) or a specific number
of seconds.
Key Benefits
Implementing SWR (Stale-While-Revalidate) and ISR (Incremental Static Regeneration) provides significant advantages for high-traffic applications:
- Zero Latency: Users never wait for the server to fetch data or render HTML because they are always served from the cache.
- Reduced Server Load: The server only renders the page once per "stale" period, regardless of how many thousands of users visit.
- High Availability: If your data source (API) goes down, Nuxt will continue to serve the "stale" cached version rather than showing an error page.
SWR vs. ISR (Incremental Static Regeneration)
While very similar, the distinction in the Nuxt/Nitro ecosystem is:
- SWR: Typically focused on caching at the Edge (CDN level). It serves the stale version immediately and refreshes the content in the background for the next visitor.
- ISR: Often implies the ability to revalidate on a schedule or via a manual trigger (webhook), though in many modern hosting providers, these terms are used interchangeably.
In Nuxt 3, the useHead composable is the primary tool for managing SEO and
document metadata (like title, meta, link
tags) programmatically. It works both on the server and the client, ensuring that search
engines see your metadata in the initial HTML.
Basic Usage of usehead
You call usehead inside the block of your page or component. It accepts an object where keys correspond to HTML head elements.
Dynamic and Reactive Metadata
Because usehead is built on top of the Unhead library, it can
accept computed properties or refs. If your data changes
(e.g., a product name is loaded from an API), the head tags will update automatically in
the browser.
Alternative: useSeoMeta
For standard SEO tasks, Nuxt provides a more concise composable called
useSeoMeta.It offers full TypeScript support for common tags, reducing the
chance of typos in property names like og:description.
Best Practices for SEO in Nuxt
To maximize search engine visibility and maintain clean meta tags, follow these core implementation strategies:
-
Placement: Use
useHeadin thepages/directory for page-specific SEO. Use it inapp.vueor a layout for global defaults. -
Server-Side Rendering: Always ensure SSR is enabled for pages where
SEO
is critical, as
useHeadensures the tags are present in the initial HTML delivered to crawlers. -
Avoid Duplication: Nuxt automatically merges head tags. If you
define
a title in
app.vueand another inabout.vue, the page-specific one will override the global one.
While both macros/composables are used within the pages/ directory, they
serve entirely different layers of the application. useSeoMeta handles what
the browser and search engines see (HTML head), while definePageMeta
handles how Nuxt treats the page internally (routing and logic).
Key Differences
| Feature | useSeoMeta |
definePageMeta |
|---|---|---|
| Primary Purpose | SEO and Social Media Metadata. | Page configuration and Routing. |
| Output Location | The <head> section of the HTML. |
Internal Nuxt/Vue Router state. |
| Reactivity | Fully Reactive: Updates if data changes. | Static: Evaluated at build/compile time. |
| Standard Tags | Title, Description, OpenGraph, Twitter. | Layouts, Middleware, Transitions, Validation. |
| Scope | Can be used in Pages and Components. | Only usable inside Files in the pages/
directory. |
Detailed Breakdown
useSeoMeta (The "External" Face)
This is a helper for SEO. It maps objects to HTML tags. It is
preferred over useHead for SEO because it provides TypeScript autocomplete
for over 100 social meta tags (like ogTitle, twitterCard).
This is a compiler macro. It tells Nuxt how to handle the page within the framework's
ecosystem. It cannot access component state (like ref) because it is
processed before the component is even instantiated.
Summary of Usage
Understanding when to use each composable is key to managing your Nuxt 3 application effectively:
-
Use
useSeoMeta: When you want to change what shows up on a Google Search result or a shared link on social media platforms like Twitter. -
Use
definePageMeta: When you want to change which layout the page uses, protect the route with a password (middleware), or validate if a URL parameter is a number.
Implementing a sitemap in Nuxt 3 is most efficiently done using the
nuxt-simple-sitemap module (now part of the Nuxt SEO suite).
It automatically generates sitemap.xmlfiles based on your page structure
and supports dynamic routes.
Method 1: Automatic Generation (Recommended)
For most projects, you want a sitemap that stays in sync with your pages/ directory automatically.
| Step | Action | Command/Code |
|---|---|---|
| 1. Install | Add the module to your project. | npm install -D @nuxtjs/sitemap |
| 2. Register | Add it to your nuxt.config.ts. |
modules: ['@nuxtjs/sitemap'] |
| 3. Configure | Set your site URL (required). | site: { url: 'https://example.com' } |
Handling Dynamic Routes
Since Nuxt cannot "guess" all your database-driven IDs or slugs (e.g.,
/products/[id]),
you must provide them so they appear in the sitemap:
-
Static SSG: If you use
npx nuxi generate, the crawler will often find links to these pages and add them automatically. - Dynamic/SSR: You can provide a function or an endpoint in nuxt.config.ts to fetch these IDs:
Key Features of Nuxt Sitemap
The @nuxtjs/sitemap module automates complex SEO requirements,
ensuring your site remains compliant with search engine standards:
- Multi-Sitemap Support: Automatically splits your sitemaps if you have more than 50,000 URLs (SEO best practice).
-
I18n Integration: If you use
@nuxtjs/i18n, the sitemap automatically generates<xhtml:link>tags for different language versions of the same page. -
Automated Metadata: It uses the last modified date of the files
to populate the
<lastmod>tag. -
Route Exclusion: You can easily hide pages (like
/adminor secret routes) from the sitemap.
Method 2: Manual (For Simple Sites)
If you prefer not to use a module, you can create a Server Route at
server/routes/sitemap.xml.ts. You would then manually write the XML string
and set the Content-Type header to header to. However, this
requires manual maintenance as you add new pages.
Yes, Nuxt supports automatic image optimization primarily through the official
Nuxt Image module (@nuxt/image). It is designed to handle the
heavy lifting of resizing, compressing, and serving images in modern formats like WebP
or AVIF.
By replacing the standard HTML tag with the
Core Components Comparison
| Component | Use Case | Best Feature |
|---|---|---|
<NuxtImg> |
Standard images. | Generates a single optimized URL with specific width/height. |
<NuxtPicture> |
High-performance images. | Uses the <picture> tag to serve different formats
(e.g., AVIF) based on browser support. |
Key Features & Optimization Techniques
The @nuxt/image module provides powerful tools to automate image delivery
and significantly improve Core Web Vitals:
- Format Conversion: Automatically converts PNG/JPG to smaller, high-quality formats like WebP or AVIF.
-
Responsive Sizes: You can define a
sizesattribute (e.g.,sm:100vw md:50vw lg:400px), and Nuxt will generate multiple versions of the image for different screen widths. - Lazy Loading: Images are natively lazy-loaded by default, meaning they only download when they are about to enter the viewport.
- Placeholders: Supports low-quality image placeholders (LQIP) or blurred effects while the main image loads, improving perceived performance.
-
Providers: Includes built-in support for various transformation
services:
- IPX (Default): A local image processor based on Sharp.
- External: Cloudinary, Fastly, Imgix, Vercel, and AWS.
Basic Usage Example
Implementation Steps
-
Install: Run
npm install @nuxt/image -
Register:Add
'@nuxt/image'to themodulesarray innuxt.config.ts. -
Use:Replace
tags with.
Nuxt is designed as a "performance-first" framework. It addresses
Core Web Vitals (CWV) by automating many of the technical optimizations
that would otherwise require manual configuration in a standard Vue.js or React
application.
Impact on Core Web Vitals
| Metric | Focus | How Nuxt Improves It |
|---|---|---|
| LCP (Largest Contentful Paint) | Loading Speed | SSR/SSG delivers HTML immediately. The Nuxt Image module prioritizes "above-the-fold" images and converts them to WebP/AVIF. |
| CLS (Cumulative Layout Shift) | Visual Stability | Automatic font optimization (Nuxt Fonts) and mandatory width/height attributes in Nuxt Image prevent elements from jumping. |
| INP (Interaction to Next Paint) | Responsiveness | Code splitting and smart pre-fetching ensure the browser isn't bogged down by heavy JS, keeping the main thread free for user input. |
Key Built-in Optimizations
1. Smart Pre-fetchingNuxt automatically observes links in the viewport (using the Intersection Observer API).
When a becomes visible, Nuxt pre-fetches the
JavaScript and data for that page in the background. By the time the user clicks, the
transition feels instantaneous.
@nuxt/fonts)
Standard web fonts often cause layout shifts (CLS) while loading. Nuxt Fonts:
- Automatically downloads and hosts Google Fonts locally.
- Generates fallback font metrics so the "blank" space matches the final font size, eliminating jumps.
Nuxt splits your application into small chunks. If a user visits /home, the
browser only downloads the JavaScript required for the homepage. This keeps the "Initial
JavaScript Execution" time low, directly benefiting INP.
Nuxt 3 uses an optimized hydration process. You can use the
The underlying server engine (Nitro) is extremely lightweight. It yields a very low TTFB (Time to First Byte), which is the foundation for a good LCP score. Whether you are on a CDN or a VPS, Nitro ensures the server responds as quickly as possible.
In Nuxt 3, both useFetch and useAsyncData are designed to
handle data fetching while preventing "double-fetching" (where the server fetches data
and then the client fetches it again during hydration).
The simplest way to think about them is: useFetch a high-level
"shortcut," while useAsyncData the low-level
"engine."
Core Comparison Table
| Feature | useFetch | useAsyncData |
|---|---|---|
| Primary Input | A URL string (e.g., /api/data). |
A Handler function (e.g., () => ...). |
| Best For | Standard REST/API requests. | Complex logic, SDKs (Firebase/Supabase), or multiple parallel calls. |
| Implementation | Wrapper around useAsyncData + $fetch. |
Generic wrapper for any async logic. |
| Auto-Keying | Generates a key based on the URL. | Generates a key based on file name/line number (manual key recommended). |
| Simplicity | High: One line for most API calls. | Medium: Requires a wrapper function. |
When to Use Which?
UseuseFetchfor Simple API Calls
If you just need to get data from a specific endpoint, useFetch is the most
efficient choice. It automatically handles the URL, reactivity, and type inference.
useAsyncData for Complex Logic
Use this when your data fetching involves more than just a single URL request. Common scenarios include:
- Multiple Requests: Fetching from two APIs and merging the results
- External SDKs:Using a library like
cms.getEntries()ordb.collection().get() - Custom Transformations: Running heavy logic on the data before it ever reaches your component state.
A Common Mistake: $fetch alone
Many developers accidentally use $fetch (Nuxt's underlying HTTP client)
inside their components without a wrapper.
- The Problem:
$fetchdoes not save its state to the Nuxt payload. This causes the data to be fetched on the server, discarded, and then fetchedagainon the client. - The Fix: Always wrap
$fetchinuseAsyncDataif it is being called during the initial page load.
Return Values
Both composables return the exact same object structure:
data: The result of the async function.pending: A boolean indicating if the data is still loading.refresh / execute: Functions to manually trigger a re-fetch.error: An error object if the request failed.status: A string (idle,pending,success, orerror).
Choosing between$fetch and useFetch is less about "what works"
and more about "where it's running." In Nuxt, the key difference is SSR
safety and payload transfer.
| Feature | useFetch (Composable) |
$fetch (Utility) |
|---|---|---|
| Best Used In | <script setup> or middleware. |
Event handlers (clicks, submits). |
| SSR Behavior | Safe: Fetches once on server, transfers data to client via payload. | Unsafe: Can trigger a "double fetch" (once on server, once on client). |
| Reactivity | Automatic: Returns reactive data,
status, and error refs.
|
Manual: Returns a raw Promise; you must manage state yourself. |
| Lifecycle | Tied to the component lifecycle. | Independent; can be used anywhere (utils, etc.). |
When to Use $fetch
You should reach for $fetch when the request is event-driven and happens
strictly on the client after the page has already loaded:
-
Form Submissions:
Sending a
POSTorPUTrequest when a user clicks "Submit." - User Interactions: Triggering an API call on a button click (e.g., "Like" a post or "Add to Cart").
- Inside Logic/Utils: When writing a helper function that isn't a Vue component but needs to make a network request.
-
API Routes:
When writing your own server-side API endpoints in
server/api/, you use$fetchto call other external APIs.
Why Avoid $fetch in
<script setup>?
If you use await $fetch() directly in the body of your
<script setup>, it triggers the following sequence:
- On the Server: Nuxt fetches the data to render the initial HTML.
-
On the Client:
During hydration, the browser executes the same script. Since
$fetchdoesn't save its result to the Nuxt "payload," the browser calls the API again. - On The Result: Your server is hit twice, which can lead to "hydration mismatches" where the client data flashes or overwrites the server data.
Summary: Rule of Thumb
-
Initial Page Data?
Use
useFetch -
User Clicked a Button?
Use
$fetch. -
Complex Logic + SSR?
Use
useAsyncData(() => $fetch(...)).
To handle API errors globally in Nuxt 3, you should use interceptors
within a custom fetch instance. Nuxt uses ofetch under the hood, which
provides lifecycle hooks like onResponseError to catch errors across your
entire app.
The most robust pattern is to create a custom fetch composable that wraps your logic in one place.
Step 1: Create a Custom Fetch Plugin
First, create a plugin to define a global API instance with interceptors. This is where you handle specific status codes (like 401 Unauthorized).
Step 2: Create a Custom Composable
To make this instance easy to use with Nuxt's SSR-friendly useFetch , wrap
it in a composable.
Comparison of Error Handling Methods
| Method | Best For | Behavior |
|---|---|---|
Interceptors (onResponseError) |
Global Actions | Best for redirects (401), logging, or refreshing tokens. |
NuxtErrorBoundary |
UI Isolation | Wraps components to show a fallback UI if a child fails. |
error.vue |
Fatal Errors | A top-level page that catches unhandled 404s or 500s. |
createError |
Manual Triggers | Used to intentionally throw an error that Nuxt can catch. |
Bonus: The NuxtErrorBoundary
Component
If you want to prevent a single component's API failure from breaking the entire page, use the built-in boundary component.
useState is a Nuxt-specific composable used for creating
SSR-friendly reactive state. While it looks and behaves similarly to a
standard Vue ref, it is specifically designed to solve the challenges of
hydration and cross-request state pollution in a server-side rendered
environment.
Key Differences: useState vs. Standard
ref
| Feature | Standard Vue ref |
Nuxt useState |
|---|---|---|
| Persistence | Lost during hydration; resets to initial value on the client. | Preserved: Value is serialized on the server and "hydrated" on the client. |
| Scope | Local to the component (unless defined globally). | Shared: Key-based system allows access across any component. |
| SSR Safety | Risk of State Leakage if defined at the module level. | Safe: Isolated per user request on the server. |
| Identification | No key required. | Requires a Unique Key to identify the state piece. |
Why You Need useState for SSR
If you use ref(Math.random()) in a component, the server will generate one
number, and the client will generate a different one during hydration. This causes a
"Hydration Mismatch" error. useState ensures the client picks up the exact
same value the server generated.
In a standard Vue SPA, a global ref is fine because each user has their own
browser. However, on a Nuxt server, a ref defined outside a component
becomes a singleton shared by every user visiting the site. This could lead to User A
seeing User B's private data. useState prevents this by scoping state to
the specific request.
When to Use Each
- Use
ref: For local, temporary UI state that doesn't need to be shared or persisted from the server (e.g., aisMenuOpentoggle). - Use
useState: For any state that needs to be shared across components or must be identical on both server and client (e.g., user authentication status, theme preferences).
Yes, Pinia is the officially recommended state management library for
Nuxt 3. While Nuxt provides useState for simple shared state, Pinia is
preferred for large-scale applications that require organized, complex logic and
developer tools support.
Pinia vs.
useState
| Feature | useState |
Pinia |
|---|---|---|
| Complexity | Simple, key-value pairs. | Structured (State, Actions, Getters). |
| DevTools | Basic support via Nuxt DevTools. | Excellent: Dedicated timeline and inspection. |
| Boilerplate | Very low. | Moderate (requires store definitions). |
| Persistence | Manual implementation needed. | Easy via plugins (e.g., pinia-plugin-persistedstate). |
| Best For | Theme toggles, simple flags. | Auth, Carts, complex Data Dashboards. |
Setting Up Pinia in Nuxt
Nuxt makes integration seamless through the @pinia/nuxtmodule.
npm install pinia @pinia/nuxt
Configuration: Add it to you modules in
nuxt.config.ts.
Usage: Define a store in the stores/ directory (Nuxt will auto-import
it).
Crucial Rule: SSR Safety
When using Pinia with Nuxt (SSR), you must follow two rules to avoid State Leakage (where one user sees another user's data):
- Avoid Global Stores:Never define a store instance outside of a
function. Always use
defineStore. - Use the Composition API:Inside your components, always call the
store hook inside
setupor .
Why use Pinia in Nuxt?
- Server-to-Client Transfer: Like
useState, Pinia handles the serialization of state. The server fetches the data, populates the store, and the client "hydrates" that exact state without re-fetching. - Modularization: It allows you to separate your business logic from your Vue components.
- HMR (Hot Module Replacement) You can edit your stores without reloading the entire page or losing the current state.
A Hydration Mismatch occurs when the DOM structure or data rendered on the server does not exactly match the DOM structure or data generated by the client during its first render.
Nuxt detects this discrepancy and "bail out" of hydration, which can lead to broken event listeners, flickering UI, or slower performance.
Common Causes and Fixes
| Cause | Example | Solution |
|---|---|---|
| Non-Deterministic Data | ref(Math.random()) or new Date() |
Use useState to sync the server-generated value to the
client. |
| Invalid HTML Nesting | <p> <div>Content</div> </p> |
Ensure your HTML is semantically valid (e.g., no div inside
p).
|
| Browser-Only APIs | window.innerWidth or localStorage |
Wrap code in onMounted or use the
<ClientOnly> component.
|
| Third-Party Plugins | Chart libraries or Google Maps | Ensure they are initialized only on the client side. |
Strategic Solutions
1. UseuseState for Consistency
If you need a random ID or a timestamp that remains identical on both sides, useState is mandatory. It serializes the value into the HTML payload so the client doesn't re-calculate it.
2. Use theFor components that rely heavily on browser-specific features (like a complex data table
or a window-width listener), wrap them in
onMounted hook
Logic inside Logic insideonly runs in the browser. Use this to modify state
that depends on window or document
How to Debug Mismatches
- Nuxt DevTools: Open the DevTools in your browser; it often highlights exactly which element caused the hydration error.
- Console Logs: Chrome will typically log: "Hydration completed but contains mismatches." It will then list the expected vs. actual HTML.
- View Source: Right-click and "View Page Source." Compare that static HTML with what you see in the "Elements" tab of your inspector.
The "Last Resort" Attribute
If you have a specific element that you know will be different (and you're okay with it),
you can use the data-hydration-uid or the Vue-specific
suppressHydrationWarnin attribute, though this should be avoided if
possible.
In Nuxt 3, useFetch (and useAsyncData) returns a set of helper
functions specifically designed for manual re-execution. The primary method for this is
the refresh function (also aliased as execute).
Ways to Trigger a Refresh
There are three main ways to re-trigger a data fetch: using the returned function, using a reactive dependency, or using a global utility.
| Method | Trigger Type | Best Use Case |
|---|---|---|
refresh() |
Manual/Imperative | Button clicks, form submissions, or interval timers. |
watch (Option) |
Reactive | Re-fetching data when a URL parameter or ID changes. |
refreshNuxtData() |
Global | Refreshing data in one component from a completely different component. |
1. Manual Refresh (The Function Call)
When you call useFetch, destructure the refresh function. You
can then call this inside any event handler.
2. Automatic Refresh (Reactive Watch)
If your API call depends on a reactive variable (like a page number or search query), use
the watch option. When the variable changes, useFetch
automatically re-executes.
3. Global Refresh (refreshNuxtData)
Sometimes you need to refresh data in a parent component after an action in a child
component (like refreshing a list after deleting an item). You can target a specific
useFetch call using its unique key.
Key Considerations
-
refreshvs.execute: These are essentially the same.executeis often used when{ immediate: false }is set in the options, meaning the fetch doesn't run until you manually call it. -
Pending State:
The
pendingref returned byuseFetchwill switch back totrueevery timerefreshis called, allowing you to show loading spinners during updates. -
Data Deduplication:
If multiple components are listening to the same key, calling
refreshNuxtDatawill update all of them simultaneously with a single network request.
In Nuxt 3, the "Lazy" version of data fetching allows your application to navigate to a new page immediately, without waiting for the data to finish loading on the client side.
By default, useFetch blocks the navigation (the browser "hangs" for a split
second) until the data is resolved. useLazyFetch breaks this block, making
the UI feel more responsive.
useFetch vs useLazyFetch
| Feature | useFetch |
useLazyFetch |
|---|---|---|
| Navigation | Blocking: Wait for data before showing the page. | Non-blocking: Show the page immediately. |
| User Experience | Best for critical data (SEO/Meta tags). | Best for "below-the-fold" or secondary data. |
| Pending State | Usually starts as false on the client. |
Starts as true on the client. |
| Implementation | Equivalent to useFetch(url). |
Equivalent to useFetch(url, { lazy: true }). |
How it Works in Practice
When you use useLazyFetch, you must handle the "loading" state in your
template, otherwise your app might try to render data that doesn't exist yet, leading to
errors.
Why Use Lazy Fetching?
- Perceived Speed: Users see the layout and navigation elements immediately, which feels faster than staring at a loading bar on the previous page.
- Hybrid Importance: If you have a sidebar that loads data from a
slow API, using
useLazyFetchensures the main content of your page isn't delayed by that slow sidebar. - Client-Side Navigation: It is particularly effective for Single Page Application (SPA) transitions where you want the "feel" of an instant app.
Key Limitation: SEO & Meta Tags
Do not use lazy fetching for data required for SEO. Because
useLazyFetchdoes not block the render, if you are using the fetched data to
populate useHead(like a page title or meta description), search engine
crawlers might see the "Loading..." state instead of your actual content. Use standard
useFetch for any data that must be present in the initial HTML.
The "Lazy" Macro
You can achieve the same result using the lazyoption in the standard
composables:
useLazyFetch(url)is identical touseFetch(url, { lazy: true }).useLazyAsyncData(key, fn)is identical touseAsyncData(key, fn, { lazy: true }).
In Nuxt 3, passing headers or authentication tokens during Server-Side Rendering (SSR) requires specific care. Unlike the browser, which automatically attaches cookies to requests, the Nuxt server acts as a "proxy" and does not share the user's browser state unless you manually forward it.
1. Forwarding Client Headers (Browser ? Nuxt Server ? API)
If your API relies on cookies or headers (like Authorization) that the
browser originally sent to Nuxt, you must "proxy" them using the
useRequestHeaders composable.
| Header Type | Implementation | Why? |
|---|---|---|
| All Headers | headers: useRequestHeaders() |
Passes everything (cookies, auth, user-agent) to the API. |
| Cookies Only | headers: useRequestHeaders(['cookie']) |
Prevents unnecessary header bloat; only sends session data. |
| Specific Header | headers: useRequestHeaders(['authorization']) |
Ideal for Bearer tokens sent from the client. |
2. Attaching Authentication Tokens (Manual)
When using JWT tokens stored in a cookie or state, you should attach them using the
Authorization header.
The useRequestFetch Pattern
For more advanced scenarios, Nuxt provides useRequestFetch. This utility
creates a version of $fetch This utility creates a version of
- When to use: When you are calling an internal Nuxt API route
(
/api/...) from within useAsyncData. - Advantage: It handles the "context forwarding" automatically so you don't have to manually map headers.
Security Warning: Avoid Header Leakage
Be extremely careful when using useRequestHeaders() with external
APIs. If you forward all client headers to a third-party service, you might
accidentally leak sensitive user cookies or internal server headers to an untrusted
source.
Tip:Always whitelist only the specific headers your API needs (e.g.,
'cookie', 'authorization').
Summary Table
| Header Type | Implementation | Why? |
|---|---|---|
| All Headers | headers: useRequestHeaders() |
Passes everything (cookies, auth, user-agent) to the API. |
| Cookies Only | headers: useRequestHeaders(['cookie']) |
Prevents unnecessary header bloat; only sends session data. |
| Specific Header | headers: useRequestHeaders(['authorization']) |
Ideal for Bearer tokens sent from the client. |
Nuxt 3 leverages its powerful server engine, Nitro, to provide a multi-layered caching system. This allows you to cache everything from entire HTML pages down to individual server functions or raw API responses.
The caching strategy you choose depends on whether you want to cache the entire route, an API endpoint, or a specific expensive function.
1. Route-Level Caching (Hybrid Rendering)
The simplest way to cache is through routeRules in
nuxt.config.ts. This tells the server how to treat specific URL patterns
without writing any custom server code.
| Strategy | Behavior | Best Use Case |
|---|---|---|
swr |
Serves stale cache while revalidating in the background. | Product listings, blog feeds. |
isr |
Similar to SWR but often used with specific TTL (Time To Live). | Dynamic content that updates hourly. |
prerender |
Generates a static HTML file at build time. | Landing pages, "About Us" pages. |
2. API & Event Handler Caching
If you have custom API routes in server/api/, you can use Nitro's
specialized handlers to cache their JSON responses.
defineCachedEventHandler: Wraps an entire API route. The first user triggers the logic; subsequent users get the cached JSON result.defineCachedFunction: Caches the result of a specific utility function used within multiple handlers.
3. Low-Level Storage (useStorage)
For manual control, Nuxt provides a unified Storage Layer called unstorage.
You can get, set, and expire data manually using different "drivers" (Memory, Redis,
Filesystem).
Summary of Caching Layers
| Layer | Tool | Controlled By |
|---|---|---|
| Browser/CDN | Cache-Control headers |
routeRules.headers |
| Page/HTML | SWR / ISR | routeRules |
| API Response | defineCachedEventHandler |
Nitro Cache |
| Logic/Data | defineCachedFunction |
Nitro Cache |
| Raw Data | useStorage |
Manual Implementation |
Nuxt Modules are the building blocks of the Nuxt ecosystem. They are essentially functions that run sequentially when Nuxt starts in development mode or during the build process. Their primary purpose is to automate the configuration and extension of your application.
Instead of manually configuring 10 different files to add a feature like Tailwind CSS or a Sitemap, a module handles the setup for you in a single line.
How Modules Extend Nuxt
Modules have deep access to the Nuxt "hooks," allowing them to modify almost any part of the framework's behavior.
| Feature | How a Module Modifies It |
|---|---|
| Auto-imports | Adds new composables (e.g., useSupabaseUser) to your
project. |
| Components | Automatically registers UI components (e.g., <Icon />
or <NuxtImg />). |
| Nitro Server | Injects new server routes, storage drivers, or middleware. |
| Build Process | Modifies Vite or Webpack configurations to handle files like
.svg or .sass.
|
| Templates | Injects code into app.vue or the HTML head (e.g., Google
Analytics scripts). |
The Three Types of Modules
- Official Modules: Maintained by the Nuxt team (e.g.,
@nuxt/image,@nuxt/content,@nuxt/ui). - Community Modules:: Maintained by the open-source community
(e.g.,
lucide-nuxt,nuxt-simple-sitemap). - Local Modules:: Custom logic written inside your own project
(usually in a
modules/folder) to keep yournuxt.config.ts.
Using a Module
To use a module, you typically follow a two-step process:
- Install the package::
npm install @nuxtjs/tailwindcss - Register it:: Add it to the
modulesarray innuxt.config.ts.
Why Not Just Use Plugins?
While they sound similar, they serve different stages of the lifecycle:
- Plugins: Run at runtime (when the app starts in the browser or on the server). Use them for things like global directives or injecting variables into the Vue instance.
- Modules: Run at build-time. Use them to change the actual structure of the app, add files, or configure the build engine.
The Module Builder
If you want to create your own module to share with others, Nuxt provides a starter kit
called nuxt-module-builder. It streamlines the process of writing, testing,
and publishing a module to the official Nuxt Modules directory.
Creating a custom Nuxt module allows you to encapsulate reusable logic, register components, or hook into the Nuxt lifecycle. You can create a local module for a single project or use the Starter Kit to build a package for NPM.
1. Creating a Simple Local Module
For internal project logic, you don't need a complex setup. You can define a module
directly in your modules/ directory.
File: modules/hello-world.ts
2. Using the Official Starter Kit (For Distribution)
If you intend to share your module via NPM, use the official template. It includes a
src/ directory for logic and a playground/ directory for live
testing.
npx nuxi init -t module my-awesome-module
Standard Module Structure:
| File/Folder | Purpose | | :--- | :--- || src/module.ts |The main entry point where you define hooks and configuration. || src/runtime/ |Contains code that will be injected into the user's app (plugins, components). || playground/ |A mini Nuxt app to test your module in real-time. || package.json |Manages dependencies and metadata (name, version). |
3. Common Module Tasks
Modules extend the framework by using Nuxt Kit>> utilities.
- Adding a Component:
addComponent({ name: 'MyButton', filePath: resolver.resolve('./runtime/MyButton.vue') }) - Injecting a Composable:
addImports({ name: 'useMyTool', from: resolver.resolve('./runtime/composables') }) - Hooking into Nuxt:
4. Key Differences: Local vs. Published
- Local: Registered in
nuxt.config.tsvia relative path (e.g.,'~/modules/my-module'). Perfect for site-specific optimizations. - Published:Installed via
npm installand registered by package name. Requires proper bundling usingnpm run prepack.
While both Modules and Plugins are used to extend Nuxt,
they operate at completely different stages of the application lifecycle. The simplest
way to remember the difference is: Modules happen at "Build-time"
(when you run npm run dev or build), while Plugins
happen at "Runtime" (when the user actually opens the page).
Core Comparison Table
| Feature | How a Module Modifies It |
|---|---|
| Auto-imports | Adds new composables (e.g., useSupabaseUser) to your
project. |
| Components | Automatically registers UI components (e.g., <Icon />
or <NuxtImg />). |
| Nitro Server | Injects new server routes, storage drivers, or middleware. |
| Build Process | Modifies Vite or Webpack configurations to handle files like
.svg or .sass.
|
| Templates | Injects code into app.vue or the HTML head (e.g., Google
Analytics scripts). |
When to Use a Module
Use a Module if you need to change how Nuxt works or if you want to bundle multiple features together.
- Automating Setup: Add tools like Tailwind CSS and configure them automatically without requiring manual setup from the user doing it manually.
- Adding Components/Composables: Provide a reusable library of UI components or composables that are auto-imported into the app.
- Modifying the Build: You need to change how Vite processes specific file types (like SVG or YAML).
- Nitro Configuration: Inject server-side middleware, storage drivers, or modify Nitro configuration globally.
When to Use a Plugin
Use a Plugin if you need to provide something to your Vue components or handle logic during page load.
- External Libraries: Initialize libraries that require access to the DOM or Vue instance (e.g., tooltip libraries or Google Analytics).
- Global Helpers: Create reusable helpers like
$formatDatethat are available in every component viauseNuxtApp(). - Injected Keys: Providing a specific value (like a global
configuration object) that components can
inject. - Route Middleware: Attaching logic that runs specifically when the router initializes
The "Parent-Child" Relationship
The "Parent-Child" Relationship Module to install a Plugin.
- Example: The
@nuxtjs/supabaseModule configures the project and then injects a Plugin so that you can useconst supabase = useSupabaseClient()inside your components.
To register a third-party library globally in Nuxt 3, you create a file in the
plugins/ directory. Nuxt automatically scans this folder and initializes
these files during the application's startup.
Depending on the library, you might need to register it as a Vue Plugin
(using .use()) or provide it as a helper accessible
throughout your app via useNuxtApp().
Common Registration Patterns
| Method | Best For | Implementation Goal |
|---|---|---|
nuxtApp.vueApp.use() |
Vue UI Libraries | Standard Vue components/directives (e.g., Vuetify, Floating Vue). |
provide |
Logic-based SDKs | Making a tool available globally as $myLibrary (e.g., Axios
instance). |
.client / .server |
Environment-specific | Preventing a library that uses window from crashing the
server. |
Implementation Examples
1. Registering a Vue Component LibraryIf you are using a library like Floating Vue for tooltips, you register it directly to the Vue instance.
File: plugins/floating-vue.ts
If you want to make a library like LDAP or a custom Analytics instance available everywhere.
File: plugins/analytics.ts
Environment-Specific Plugins
Many third-party libraries (like Chart.js or Google Maps) require access to the
window or document objects. If these run on the server, the
app will crash. You can control this using file suffixes:
my-plugin.client.ts: Runs only on the browser side.my-plugin.server.ts: Runs only on the server side.
Best Practices
Follow these best practices when working with plugins in Nuxt:
- Avoid Overuse: Every plugin adds to the "Initial Bundle Size." Only register libraries globally if they are used on almost every page.
- Tree Shaking: If a library supports it, prefer importing it locally within the component where it is needed instead of a global plugin.
- Order Matters: If one plugin depends on another, prefix filenames
with numbers (e.g.,
01.auth.ts,02.api.ts) to control execution order.
The server/ directory is what makes Nuxt 3 a full-stack
framework. It allows you to write backend logic—like API endpoints,
database connections, and server-side security—directly within your Nuxt project.
Everything in this folder is powered by Nitro, Nuxt's high-performance server engine, and it runs in a Node.js (or Edge) environment, completely separate from your Vue frontend code.
Core Structure of the
server/ Directory
| Sub-directory | Purpose | Access URL |
|---|---|---|
api/ |
Standard JSON API endpoints. | /api/your-file |
routes/ |
Custom server routes without the /api prefix. |
/your-file |
middleware/ |
Code that runs on every server request. | N/A (Internal) |
plugins/ |
Extends Nitro's runtime (e.g., connecting to a DB). | N/A (Internal) |
utils/ |
Shared helper functions for server-side code. | N/A (Internal) |
1. The api/ Directory (File-Based
API)
Any file you place here automatically becomes an API endpoint. You use
defineEventHandler to process requests.
- Result: Sending a GET request to
/api/helloreturns the JSON object above. - Method Support: You can specify methods using suffixes like
hello.get.tsoruser.post.ts.
2. Server Middleware
Unlike frontend route middleware, server middleware runs for every single request made to the server (including requests for images or static assets). It's the perfect place for:
- Logging requests.
- Checking authentication headers.
- Adding custom context to the
eventobject.
3. Why Use the server/ Folder?
- Security: Keep sensitive data like API keys or database credentials hidden from the browser.
- Full-Stack Power: Handle tasks such as sending emails or processing Stripe payments without needing a separate backend like Express or Laravel.
- Type Safety: Nuxt auto-generates TypeScript types for API routes,
so
useFetch('/api/hello')knows exactly what data is returned. - Performance: Nitro is highly optimized and can be deployed to serverless platforms like Vercel, Netlify, or Edge workers such as Cloudflare.
The "Server Utils" Advantage
Files in server/utils/ are auto-imported within your server handlers. If you
create a database connection utility there, you can use it in any file inside
server/api/ without a single import statement.
In Nuxt 3, API routes are created by placing files inside the server/api/
directory. These routes are powered by Nitro and use a file-based
routing system similar to your Vue pages.
Every API route must export a defineEventHandler function. This function
receives an event object, which contains the request, response, and various
utilities.
- Endpoint:
/api/hello - Response:
{ "message": "Hello from Nitro!" }
Handling Different HTTP Methods
You can handle specific methods (GET, POST, DELETE, etc.) by either checking the method inside the handler or using the filename suffix.
| Filename Strategy | Method | Use Case |
|---|---|---|
submit.post.ts |
POST | Specific file for form submissions. |
data.get.ts |
GET | Specific file for fetching data. |
user.ts |
Any | Catch-all for all methods on that route. |
Reading Request Data
Nitro provides several built-in utilities to extract data from the incoming request
1. Query Parameters
For URLs like /api/search?q=nuxt, use getQuery.
2. Request Body (POST/PUT)
To read JSON data sent in a request body, use readBody.
3. Route Parameters (Dynamic API)
For dynamic paths like /api/users/123, name your file
server/api/users/[id].ts and use event.context.params.
Advanced API Features
- Error Handling: Use
createErrorto return specific HTTP status codes. - Runtime Config: Access your private environment variables using
useRuntimeConfig(event). - Cookies: Use
getCookie(event, 'name')andsetCookie(event, 'name', value)to manage sessions.
| Utility | Purpose |
|---|---|
getQuery(event) |
Parses URL search parameters. |
readBody(event) |
Parses the JSON body of a POST/PUT request. |
getHeader(event, name) |
Retrieves a specific request header. |
setResponseStatus(event, code) |
Manually sets the HTTP status code (e.g., 201). |
In Nuxt, Middleware is code that runs before a specific event, typically a
page navigation or a server request. Although they share a name, Route
Middleware and Server Middleware are entirely separate systems
that run in different environments.
Comparison: Route vs. Server Middleware
| Feature | Route Middleware | Server Middleware |
|---|---|---|
| Environment | Hybrid (Server & Client) | Server-Only (Nitro) |
| Trigger | Triggered by page navigation. | Triggered by every server request. |
| Primary Use | Auth guards, role checks, dynamic redirects. | Logging, custom headers, body parsing. |
| Location | middleware/ |
server/middleware/ |
| Logic Type | Vue/Nuxt-based (uses useAuth, etc.). |
Nitro/H3-based (uses event object). |
1. Route Middleware (The "Frontend" Guard)
Route middleware runs within the Vue part of your application. It is ideal for logic that must happen before a user sees a page.
- Global: Runs on every route change
(e.g.,
middleware/auth.global.ts). - Named: Defined in the
middleware/folder and manually applied to specific pages usingdefinePageMeta.
2. Server Middleware (The "Backend" Interceptor)
Server middleware runs on the Nitro server. It intercepts every
single request to your server, including API calls (/api/...), static
assets, and page requests.
Warning
Server middleware is global and cannot be restricted to specific pages. It should be used sparingly for "universal" tasks like adding security headers or logging telemetry.
When to Use Which?
- Use Route Middleware when you want to protect a
specific page (e.g.,
/dashboard) or check if a user has permission to view a UI component. - Use Server Middleware when you need to modify the response of an API endpoint or handle low-level server logic that doesn't care about Vue components.
To implement a protected route in Nuxt 3, you create a Named Route Middleware. This middleware checks for a user's session or authentication token and redirects them if they aren't authorized.
The 3-Step Implementation1. Create the Middleware
Create a file in your middleware/ directory. Nuxt will automatically
register this as a named middleware.
File: middleware/auth.ts
2. Apply to Specific Pages
File: pages/dashboard.vue
3. (Optional) Create a Global Guard
If you want to protect every page by default (except for login), rename the file with
a .global suffix.
File: middleware/auth.global.ts
Best Practices for Route Guards
| Feature | Route Middleware | Server Middleware |
|---|---|---|
| Environment | Hybrid (Server & Client) | Server-Only (Nitro) |
| Trigger | Triggered by page navigation. | Triggered by every server request. |
| Primary Use | Auth guards, role checks, dynamic redirects. | Logging, custom headers, body parsing. |
| Location | middleware/ |
server/middleware/ |
| Logic Type | Vue/Nuxt-based (uses useAuth, etc.). |
Nitro/H3-based (uses event object). |
Comparison: Where to apply Auth?
Nuxt Layers are a powerful architectural feature that allows you to extend a "base" Nuxt application into other projects. Think of a layer as a fully functional Nuxt project that can be merged into another project, sharing everything from components and composables to server routes and configuration.
This is the foundation for creating multi-tenant applications, white-label solutions, or complex enterprise monorepos.
How Layers Work
When you extend a layer, Nuxt performs a "deep merge" of the two projects. If both
the layer and the child project have a components/ folder, Nuxt
combines them. If there is a conflict (e.g., both have a Header .vue),
the child project overrides the layer.
| Feature | Behavior in Layers |
|---|---|
| Components & Composables | Automatically merged and auto-imported. |
| Pages & Middleware | Merged; child pages take priority over layer pages. |
| Server Engine (Nitro) | Server API routes and middleware are merged. |
nuxt.config.ts |
Configurations are deep-merged (arrays are concatenated, objects merged). |
| Public Assets | Files in public/ are merged. |
Implementation: Extending a Layer
To use a layer, you simply point to its directory (local) or its theme name (remote)
in your nuxt.config.ts.
Key Use Cases
1. Enterprise MonoreposIf you have five different internal tools, you can create a core-layer containing the
company’s design system, authentication logic, and API utilities. Each tool then
extends this core layer, ensuring consistency and reducing code
duplication.
You can build a "Base Product" as a layer and then create multiple "Brand" projects that extend it. Each brand project only needs to override specific CSS, logos, or localized text while inheriting all the core functionality.
3. Reusable Starters (Themes)Instead of cloning a "Starter Template" (which becomes stale), you can extend a starter layer. When the starter layer is updated, your project receives the updates via a simple package pull.
Layers vs. Modules
While they seem similar, their intent is different:
- Modules Best for integrating tools or adding specific technical features (e.g., Image optimization, Sitemaps).
- Layers: Best for sharing application logic and structure (e.g., a whole "Blog" system or "Auth" system).
In Nuxt 3, environment variables are managed through the Runtime
Config. This system is superior to standard process.env
because it allows for a clear separation between variables that are safe for the
browser and those that must remain hidden on the server.
1. Defining Variables in
nuxt.config.ts
You define your variables within the runtimeConfig block. Nuxt distinguishes between private and public keys based on how they are nested.
| Configuration Key | Accessibility | Best For |
|---|---|---|
runtimeConfig (Top-level) |
Server-only | Secret API keys, DB passwords, private tokens. |
runtimeConfig.public |
Client & Server | Public API URLs, Firebase keys, Analytics IDs. |
2. Using.evuFiles
In your project root, create a .env file. Nuxt automatically maps these
to your runtimeConfig based on a naming convention.
- Prefix: Use
NUXT_ - Case: Uppercase
- Nesting: Use double underscores
__for nested public variables.
3. Accessing Variables in the App
Use the useRuntimeConfig() composable to access your values.
server/api):
The server has access to both public and private keys.
4. Why is this secure?
- Serialization Control: Nuxt only serializes
the
publicobject into the payload sent to the browser. The top-levelruntimeConfignever leaves the server environment. - Runtime Overrides: You can change these variables in production (e.g., on Vercel or Docker) without rebuilding your app, simply by updating the environment variables on the host.
Security Checklist
- Do not use
process.envdirectly in your Vue components; it is not reactive and can lead to unexpected behavior in SSR. - Add
.evuto.gitignoreto prevent secrets from being pushed to version control.Validate critical variables in aserver/pluginsfile to ensure the app doesn't start if a database URL is missing.
Nitro is the next-generation server engine built specifically for Nuxt 3. While Nuxt 2 relied on a heavy, Node-only server, Nitro is designed to be platform-agnostic, allowing Nuxt applications to run virtually anywhere—from traditional servers to "The Edge" (like Cloudflare Workers).
Core Characteristics of Nitro
Nitro is significant because it transforms Nuxt from a simple frontend framework into a high-performance, full-stack deployment powerhouse.
| Feature | Description | Benefit |
|---|---|---|
| Cross-Platform | Generates specific output for Vercel, Netlify, Docker, AWS, etc. | Deploy once, run anywhere without code changes. |
| Serverless Optimized | Minimal cold-start times and tiny bundle sizes. | Better performance on AWS Lambda and Vercel. |
| Hybrid Rendering | Supports SSR, CSR, SWR, and ISR simultaneously. | Mix static and dynamic content on a per-route basis. |
| Storage Layer | A unified API (unstorage) for Redis, FS, or Memory.
|
Switch databases or cache drivers with a config change. |
Why Nitro is a Game-Changer
1. The "Edge" RevolutionBefore Nitro, running a Vue app with SSR on the Edge (geographically close to the user) was extremely difficult due to Node.js dependencies. Nitro uses a portable runtime that doesn't rely on Node.js-specific APIs, enabling your site to load in milliseconds globally.
2. Cold Start PerformanceNitro uses Rollup to bundle your server code. By removing unused code and dependencies (tree-shaking), it produces a server entry point that is significantly smaller than traditional setups, resulting in near-instant "cold starts" in serverless environments.
3. API EngineNitro provides the engine for the server/ directory. It offers:
- Auto-imports: No need to import
defineEventHandlerorreadBody. - Zero-config: API routes are automatically mapped and typed.
- Direct calling: When the client fetches a Nitro API route during SSR, Nitro calls the function directly instead of making an actual HTTP request, saving network overhead.
Key Nitro Utilities
When working in the server/ folder, you are interacting directly with
Nitro/H3 utilities:
useStorage(): For persistent or temporary data storage.useRuntimeConfig(): Accessing environment variables on the server.defineCachedEventHandler(): For built-in server-side caching.
Nitro Deployment Presets
You don't have to configure your server for different hosts. Nitro detects your
environment or lets you specify it in nuxt.config.ts:
Implementing Lazy Loading for heavy components is one of the most
effective ways to reduce your initial JavaScript bundle size and improve your site's
"Time to Interactive" (TTI). In Nuxt 3, this is primarily achieved using the
Lazy prefix.
The Three Methods of Lazy Loading
| Method | Implementation | Best Use Case |
|---|---|---|
| Lazy Prefix | <LazyMyHeavyChart /> |
Modals, footers, or complex widgets toggled by user action. |
| Dynamic Import | defineAsyncComponent() |
When you need advanced control (loading/error states). |
| Lazy Hydration | hydrate-on-visible |
Content that is on the page but doesn't need to be interactive immediately. |
1. The Lazy Prefix
(Nuxt-Specific)
Nuxt automatically creates a separate JavaScript chunk for any component when you add
the Lazy prefix to its name in the template. The code is only
downloaded when the component is rendered.
2. Advanced Control with
defineAsyncComponent
If you want to show a custom Loading Spinner while the heavy
component is being downloaded, or handle a Timeout, use the Vue
standard method.
3. Lazy Hydration (Nuxt 3.16+)
This is a newer feature that allows a component to be rendered on the server (good for SEO) but delays its interactivity on the client. This prevents "heavy" JS from blocking the main thread during initial load.
Best Practices & Pitfalls
-
Always use v-if: If you use
v-showwith a Lazy component, the code will be downloaded immediately becausev-showkeeps the element in the DOM (just hidden). - Don't Lazy Load Above-the-Fold: Never lazy load your main hero section or header. This can cause layout shifts and hurt your Largest Contentful Paint (LCP) score.
- Use prefetchComponents: If you know a user is likely to click a button (e.g., hovering over it), you can manually start the download:
Nuxt Islands (also known as Server Components) are a specialized way to render components only on the server. Unlike standard Vue components that send JavaScript to the browser to become interactive ("hydration"), Islands send pure HTML and CSS to the client.
This architecture allows you to create "islands" of static content within an otherwise dynamic application, drastically reducing the amount of JavaScript the user has to download.
How Islands Differ from Standard Components
| Feature | Standard Component | Nuxt Island (Server Component) |
|---|---|---|
| Rendering | Server + Client (Hydration) | Server-only (No Hydration) |
| JavaScript Sent | Full component logic + dependencies. | Zero JavaScript. |
| Interactivity | Fully reactive (clicks, state). | Non-interactive (Static HTML). |
| Data Fetching | Can happen on both sides. | Only happens on the server. |
| Best For | Forms, Dashboards, Interactivity. | Blogs, Marketing sections, Static Lists. |
1. How to Use Islands
To create an island, you simply suffix your component filename with
.server.vue.
File components/StaticReport.server.vue
2. The NuxtIsland Component
You can also load islands dynamically using the
3. Why use Islands?
- Massive Performance Gains: If you have a component that uses a heavy library (like a Markdown parser or a Syntax Highlighter), using an Island means the library never reaches the client's browser.
- SEO Friendly: Since the output is pure HTML, search engines can index the content perfectly without needing to execute JavaScript.
- Hybrid Interactivity: You can nest standard interactive components inside an Island using "Slots." The Island remains static, but the "Slot" content can be interactive.
Key Constraints
-
No Client Logic: You cannot use
onMounted,watch, or event listeners like@clickinside a.server.vuecomponent. -
Experimental: As of recent versions, this feature is still
considered experimental. You must enable it in your
nuxt.config.ts:
In Nuxt 3, handling errors is divided into two parts: catching application-level crashes (500 errors) and handling missing routes (404 errors).
1. The Custom Error Page
(error.vue)
To create a global error interface, place a file named error.vue in the
root directory of your project (alongside app.vue). This page acts as a
"fallback" whenever Nuxt encounters an unhandled exception or a manual error.
File: /error.vue
2. Triggering Errors Manually
You can force the application to show the error page by using the
createError or showError utilities.
| Utility | Behavior | Best Use Case |
|---|---|---|
createError |
Returns an error object. | Used inside useFetch or useAsyncData to
stop execution. |
showError |
Immediately triggers the full-screen error page. | Used in client-side logic (e.g., after a failed form submission). |
clearError |
Clears the error state and redirects. | Used on the error page "Close" or "Home" button. |
Note
Setting fatal: true ensures the error is caught during
client-side navigation.
3. Handling 404s Specifically
While error.vue handles all errors, you may want a "Page Not Found"
layout that stays within your site's standard design. You can achieve this using a
Catch-all Route.
File: pages/[...slug].vue
4. Error Boundaries
(NuxtErrorBoundary)
If you don't want the entire page to crash when a small component fails, use an Error Boundary. This allows you to catch errors locally and show a fallback UI while keeping the rest of the page interactive.
Summary of Strategies
| Scenario | Recommendation |
|---|---|
| Generic Crash | Create error.vue in the root. |
| Route missing | Create pages/[...slug].vue. |
| API data missing | Use throw createError({ statusCode: 404 }). |
| Component failure | Wrap in <NuxtErrorBoundary>. |
Deploying a Nuxt 3 application is highly streamlined thanks to the Nitro engine, which automatically detects your deployment environment and configures the output accordingly.
1. Automated Deployment (Recommended)
Both Vercel and Netlify offer "Zero Config" deployments for Nuxt. The process is nearly identical for both platforms:
- Push your code to a Git provider (GitHub, GitLab, or Bitbucket).
-
Import the projectin the Vercel or Netlify dashboard. -
Automatic Detection: The platforms will detect Nuxt 3 and set
the following defaults:
-
Build Command:
npm run buildornpx nuxi build -
Output Directory:
.output/public(for static) or the platform's native function directory (for SSR).
-
Build Command:
-
Configure Environment Variables: Add any
NUXT_PUBLIC_*or secret keys in the platform's "Environment Variables" settings.
2. Deployment Methods: SSR vs. Static
Depending on your nuxt.config.ts, Nitro will package your app
differently.
| Method | Build Command | Output | Best For |
|---|---|---|---|
| SSR / Hybrid | npm run build |
Serverless Functions | Dynamic apps, personalized content, and APIs. |
| SSG (Static) | npm run generate |
Static Files | Blogs, documentation, and SEO-heavy sites. |
3. Platform-Specific Notes
VercelVercel is the native home of many Nuxt developers. It handles ISR (Incremental Static Regeneration) exceptionally well.
- Edge Functions: You can run your Nuxt app on the Edge by
setting
nitro: { preset: 'vercel-edge' }in your config. - Analytics: Supports Vercel Speed Insights and Analytics out of the box.
Netlify is excellent for high-traffic static sites and offers robust "Edge Rules."
- Netlify Functions: Your server logic is automatically converted into Netlify Functions.
- You can manage
_redirectsand_headersdirectly viarouteRulesin Nuxt.
4. Manual Preset Selection
If the auto-detection fails or you want to force a specific behavior, you can define
the preset in your nuxt.config.ts:
5. Deployment Checklist
-
[ ] Runtime Config: Ensure all
.envvariables are added to the provider's dashboard. - [ ] Node Version: Ensure the provider is using Node.js 18.x or later (Nuxt 3 requirement).
-
[ ] Build Health: Run
npm runbuild locally once to ensure there are no Nitro bundling errors before pushing. -
[ ] Trailing Slashes: Check your
routeRulesif you have specific SEO requirements for URL structures.
In Nuxt 3, these three commands represent the primary stages of the application lifecycle: Development, Production (Dynamic), and Production (Dynamic).
Choosing the right command depends on whether you are currently coding or ready to deploy your site to the web.
Core Comparison Table
| Command | Environment | Output | Primary Goal |
|---|---|---|---|
npm run dev |
Local Development | Hot-reloaded code in memory. | Instant feedback while coding. |
npm run build |
Production (SSR) | Executable Nitro server (.output). |
Dynamic apps requiring a server. |
npm run generate |
Production (SSG) | Static HTML/JS files (.output/public). |
Static hosting (S3, GitHub Pages). |
1. npm run dev (Development)
This starts a local development server (usually at localhost:3000). It
is optimized
for speed and developer experience.
- Hot Module Replacement (HMR): Changes in your components are reflected instantly without a full page reload.
- Error Reporting: Provides detailed stack traces and the Nuxt DevTools overlay.
- Server: Uses a local Nitro development server to handle API routes and SSR on the fly.
2. npm run build (Production SSR)
This command prepares your application for a Universal (SSR) deployment. It compiles your Vue code and your Nitro server logic into a production-ready bundle.
-
Output: Generates a
.outputfolder containing a standalone Node.js server. - Behavior: The server will dynamically render pages and process API requests every time a user visits.
- Best For: Applications with frequently changing data, user authentication, or complex API logic.
3. npm run generate (Production
SSG)
This command triggers Static Site Generation. Nuxt crawls every route in your application and saves the result as a static HTML file.
-
Output: Generates static files in
.output/public. No Node.js server is required for hosting. - Behavior: All data fetching is done at build time. When a user visits, they receive pre-rendered HTML.
- Best For: Blogs, documentation sites, and marketing pages where speed and SEO are the highest priorities.
Which one should you use?
-
While coding: Use
dev. -
Deploying to Vercel/Netlify (Dynamic): Use
build. The platform will automatically handle the serverless execution. -
Deploying to a "Static Only" host: Use
generate.
Pro Tip: In Nuxt 3, npm run generate is actually a
shortcut for nuxt build --prerender. It still uses Nitro under the hood
to "pre-render" your routes into static files.
Nuxt DevTools is one of the most powerful utilities in the Vue ecosystem, providing deep insights into your app's performance and structure directly within your browser.
To open it, ensure you are in development mode
(npm run dev) and click the Nuxt icon at the bottom of your screen or
press Shift + Alt + D.
Key Performance Debugging Features
| Feature | What it Monitors | How to use it for Performance |
|---|---|---|
| Timeline | Component renders and hydration. | Identify "expensive" components that take too long to mount. |
| Payload | Server-to-client data transfer. | Check if you are sending too much unused data in
useAsyncData.
|
| Assets | Image and font sizes. | Find unoptimized images that are slowing down the page load. |
| Nitro | Server-side tasks and API calls. | Debug slow API responses or server-side logic bottlenecks. |
1. Using the Timeline for Hydration
The Timeline tab is essential for identifying Hydration Mismatches and slow component mounting.
- Look for: Long bars in the timeline. If a component takes 100ms+ to render, it might need to be optimized or lazy-loaded.
- Action: If a component is visually "static" but takes a long time to hydrate, consider using Nuxt Islands or Lazy components.
2. Analyzing the Payload
When you fetch data on the server, Nuxt "serializes" it into a JSON object to pass it to the client.
- Look for: Massive JSON objects in the Payload tab.
-
Action: Use the
transformorpickoptions inuseFetchto only return the keys you actually need. This reduces the HTML size significantly.
3. Inspecting Server Routes (Nitro)
Under the Server Routes tab, you can see every API endpoint available in your server/ directory.
- Test Performance: You can trigger API calls directly from the DevTools to see their response time without refreshing the frontend.
- Storage: Check the Storage section to see what is currently in your Nitro cache (Redis, memory, etc.).
4. Component Inspector
You can click the Inspector icon and hover over any element on your page.
- It will show you exactly which Vue component rendered that element.
- This is helpful for finding "hidden" heavy components that are nested deep within your layout.
If you don't see the icon, ensure it's enabled in your nuxt.config.ts:
Hydration is the process where a client-side JavaScript application "takes over" a static HTML page that was rendered by the server.
When you visit a Nuxt site, the server sends a fully rendered HTML document so the user sees content immediately. However, this HTML is "dead"—buttons don't work, and there is no reactive state. Hydration is the bridge that turns that static HTML into a live, interactive Vue application by connecting the browser's DOM to the Vue virtual DOM.
The Hydration Workflow
| Step | Action | Environment |
|---|---|---|
| 1. Render | Nuxt converts Vue components into an HTML string. | Server |
| 2. Delivery | The browser receives and displays the HTML + CSS. | Browser |
| 3. Download | The browser downloads the JavaScript bundles. | Browser |
| 4. Hydrate | Vue scans the HTML, builds the internal state, and attaches event listeners. | Browser |
Why Hydration Fails (Mismatches)
A Hydration Mismatch occurs when the HTML generated by the server does not exactly match the HTML the client expects to see. When this happens, Vue has to "bail out," discard parts of the server HTML, and re-render them, which causes a flicker and hurts performance.
class="h5-equivalent">Common Causes of Mismatches
- Invalid HTML Structure: Browsers automatically "fix" bad HTML (e.g., putting a div tag inside a p tag). Since the server sends the raw "bad" HTML and the browser fixes it before Vue hydrates, the structures no longer match.
- Environment-Specific Logic: Using checks like
if (window)orif (process.client)inside your template.
window is, so it renders nothing. The
client renders "Desktop." Mismatch!- Dates and Times: Rendering
new Date()directly in a template. The server time and the client time will always be slightly different. - Randomness: Using
Math.random()to generate IDs or values in a template.
How to Fix Hydration Issues
| Strategy | Tool/Component | Purpose |
|---|---|---|
| ClientOnly | <ClientOnly> |
Wraps components that should only render on the client (e.g., a map). |
| onMounted | onMounted() hook |
Moves logic that depends on the browser to after hydration is complete. |
| useId | useId() |
A Nuxt 3.10+ composable that generates stable IDs for both server and client. |
| Suppressing Warnings | data-hydration-metadata |
(Internal) Helps Nuxt track specific nodes for debugging. |
Debugging Mismatches
- Check the Console: Chrome will show a warning: "Hydration completed but contains mismatches." It usually points to the specific tag that failed.
- Nuxt DevTools: The Timeline tab highlights hydration events and can help you spot which component triggered the warning.
- View Source: Compare the "View Page Source" (Server HTML) with the "Inspect Element" (Client DOM) to find the difference.
Integrating a Headless CMS with Nuxt 3 is typically handled through
data-fetching composables (useFetch or useAsyncData) or
dedicated modules. The goal is to fetch content from the CMS's API and map it to
your Vue components.
The Two Primary Integration Strategies
| Method | Best For | Implementation |
|---|---|---|
| Official Modules | Popular CMSs (Strapi, Contentful, Sanity) | Use a pre-built module like @nuxtjs/strapi. |
| Generic SDK / API | Less common CMSs or Custom APIs | Use the CMS’s JavaScript SDK or standard HTTP fetches. |
1. Using a Dedicated Module (e.g., Strapi)
Dedicated modules handle authentication, base URLs, and TypeScript integration automatically.
Setup:
- Install:
npm install @nuxtjs/strapi - Configure in
nuxt.config.ts:
2. Using a Generic SDK (e.g., Contentful)
.If a module isn't available or you prefer the official SDK, you can initialize the
client in a Nuxt Plugin or a Server Utility.
Create a utility in server/utils/cms.ts to keep your API keys secure.
3.optimizing for SEO and speed
-
Static Site Generation (SSG): Use
npm run generate. Nuxt will fetch your CMS content during the build and save it as static HTML. -
Incremental Static Regeneration (ISR): Use
routeRulesto re-fetch CMS data every X minutes without rebuilding the whole site. -
Image Optimization: Use the
@nuxt/imagemodule. Most Headless CMSs (like Contentful/Strapi) have built-in image providers that allow you to resize images on the fly via URL parameters.
4.Handling Dynamic Previews
- Headless CMSs usually provide a "Preview" mode to see drafts.
- Create a "Preview" route (e.g., /api/preview).
-
Use
setCookieto enable a "preview mode" flag. - In your data fetching, check for the cookie to switch between the Production API and the Preview API.
Moving from Nuxt 3 to Nuxt 4 is an "evolution, not a revolution." Unlike the massive rewrite required for Nuxt 2 to 3, Nuxt 4 focuses on refining performance, tightening type safety, and cleaning up the project structure.
Here are the key breaking changes and architectural shifts to expect.
The most visual change is the reorganization of your project files. By default, Nuxt
4 moves application-specific folders into a dedicated app/ directory to
separate them from server logic and root-level config.
| Nuxt 3 Location | Nuxt 4 Location | Notes |
|---|---|---|
pages/ |
app/pages/ |
All frontend routing. |
components/ |
app/components/ |
All Vue components. |
app.vue |
app/app.vue |
The entry point moved into app/. |
server/ |
server/ |
Remains at the root (outside app/). |
public/ |
public/ |
Remains at the root. |
Pro Tip: This change is optional initially. Nuxt 4 will auto-detect your old structure, but migrating to the app/ folder improves file-watching performance on Windows and Linux.
Data Fetching: Shallow Reactivity & Key Sharing
Nuxt 4 streamlines useFetch and useAsyncData to be more performant and predictable.
- by Default: Data returned from these composables is now a shallowRef. It only triggers updates if the entire object is replaced. If you need deep reactivity (e.g., updating a nested property and expecting the UI to react), you must explicitly set { deep: true }.
- Keys: If two components call useFetch with the same key, they now share the same reactive state. If one component refreshes the data, the other updates automatically.
- Null to Undefined: The default value for data and error is now undefined instead of null before the fetch completes.
3. Stricter Type Boundaries
Nuxt 4 introduces Project References for TypeScript. It now treats your project as three distinct TypeScript "sub-projects":
- App: Your Vue/client code.
- Server: Your Nitro/API code.
- Shared: Code accessible by both.
The Impact: You can no longer accidentally import server-only types into your client-side components (and vice versa), which prevents "type leak" bugs that were common in Nuxt 3.
4. Component Naming Standardization
In Nuxt 3, component names in Vue DevTools or when used as strings (like in
<component :is="...">) could be inconsistent. Nuxt 4 standardizes
this:
- Names are now strictly generated based on their path and filename.
- This ensures that what you see in the code matches exactly what you see in the DevTools and Vue’s internal registry.
5. Removal of Legacy & Experimental Features
- Several features that were "experimental" in Nuxt 3 have been removed or made the permanent default:
-
Removed:
window.__NUXT__(Global data is now handled viauseNuxtApp()). -
Removed: Support for
.ejstemplate compilation. -
Locked Features: Options like
treeshakeClientOnlyandconfigSchemaare now permanently enabled and can no longer be toggled off.
You can start moving toward Nuxt 4 today while still on Nuxt 3 by enabling the "future" flags in your config: