What is Nuxt? An overview

Sunday, June 30th 2024

nuxt

Nuxt.js is an open-source framework for building full-stack web apps and websites using Vue.js. As a meta-framework, Nuxt builds upon Vue.js, adding structure, features, and optimizations to enhance the development experience and application performance.

It is used by companies like OpenAI, TikTok or Upwork. Its main feature is SSR (Server-Side Rendering), which allows you to render Vue components on the server before sending them to the client.

This approach significantly improves SEO (Search Engine Optimization) and initial page load performance.


Personally, I've been using it for a couple of projects, and I've come to love it.

Features

Nuxt comes with everything included for building complex and enterprise-grade applications and websites.

File-based Routing

Instead of managing your routes in a separate file, Nuxt builds your routes based on how they are structured in the /pages directory. This is quite convenient compared to dealing with Vue Router manually.

You can also define Layouts for your application. For example, you might have a layout where you do not have a footer or navigation bar.

pages/
├── index.vue
├── products.vue
└── contact.vue

Here we can see an example of the file-based routing. Nuxt will generate the following routes: /products, /contact and / (index). It's intuitive, clean, and saves you valuable development time.

It's that simple. No additional configuration required.

Nested Routes and Dynamic Segments

But wait, there's more! Nuxt's routing system is as flexible as it is powerful. Need nested routes?

Create a directory structure like this:

pages/
├── index.vue
└── blog/
    ├── index.vue
    ├── [id].vue
    └── category/
        ├── index.vue
        └── [...slug].vue
  • / (home page)
  • /blog (blog index)
  • /blog/:id (individual blog post, where :id is dynamic)
  • /blog/category (blog category index)
  • /blog/category/:slug* (catch-all route for categories)

The [id].vue file creates a dynamic segment, perfect for blog posts or something like product pages. The [...slug].vue file is a catch-all route, ideal for handling multiple parameters.

Route Groups

pages/
├── index.vue
└── (legal)/
    ├── privacy.vue
    └── cookies.vue

As of Nuxt 3.13 you can use route groups to, ...group routes!

Layouts

Nuxt doesn't stop at routing. It also provides a powerful layout system. Layouts allow you to define a common structure for your pages, keeping your code DRY (Don't Repeat Yourself).

For instance, you might have:

  • A default layout with a header and footer
  • A minimal layout for your blog posts
  • A fullscreen layout

To use a specific layout, just add a layout property to your page component:

<script setup lang="ts">
definePageMeta({
  layout: "blog",
})
</script>

This flexibility allows you to maintain a consistent look across your site while adapting to the needs of individual pages.

If you only have a default.vue layout, you don't need to specify it in your pages.

<script setup lang="ts">
definePageMeta({
  layout: false,
})
</script>

You can also disable the layout for a specific page, which is useful for a login page for example.

Auto-Imports

If you've used Vue or any other JavaScript framework, you will know the pain of managing big blocks of imports at the top of each file.

Nuxt solves this issue by auto-importing common things like Components, Directives (e.g. ref, computed), Composables (/composables) and Utility functions (/utils).

Before (Vue):

<script setup>
import { ref, computed } from "vue"
import Navbar from "@/components/NavBar.vue"
import { useBlogs } from "@/composables/useBlogs"
import { isEven } from "@/utils/isEven"

// code
</script>

After (Nuxt):

<script setup>
// No imports needed! Everything is auto-imported
const ref = ref(0)
const double = computed(() => ref.value * 2)
const { blogs } = useBlogs()
</script>

Whenever I go back to a Vue project without Nuxt (or most other frameworks), I really miss this feature.

Middleware

Nuxt allows you to execute code before navigating to a specific route. A common use case is checking if the user is logged in for protected routes.

Here's a code snippet from one of my projects, where I implemented simple authentication middleware:

export default defineNuxtRouteMiddleware((to, from) => {
  // redirect to login if not authenticated
  if (!authenticated && to.path !== "/login") {
    return navigateTo("/login")
  }

  // prevent from going to the login page when logged in
  if (authenticated && to.path === "/login") {
    return navigateTo("/")
  }
})

Modules

Nuxt modules extend the core functionality of your Nuxt application, allowing you to easily integrate third-party libraries.

Modules can simplify tasks such as handling authentication, or adding UI components. For example, to add Tailwind CSS to your project, you can install the module and Nuxt will handle the rest:

npx nuxi@latest module add tailwindcss

Then add it to your nuxt.config.ts:

export default defineNuxtConfig({
  modules: ["@nuxtjs/tailwindcss"],
})

They are highly configurable, enabling you to tailor their behavior to fit your specific needs. Even better, most of them work without any configuration completely.

I'd recommend you to check out the Modules page!

Data Fetching

Nuxt offers improved data fetching methods compared to the standard Fetch API. These methods provide better integration with the Nuxt lifecycle, automatic error handling, and improved performance through caching and deduplication. Here are the three main ways to fetch data in Nuxt:

  • useFetch: A composable function that fetches data on both client and server-side. It is ideal for component-level data fetching and provides reactive properties for easy handling of loading, error, and data states.
  • useAsyncData: Designed for wrapping asynchronous logic, useAsyncData resolves data fetches and returns results upon completion. It's noteworthy that useFetch is nearly analogous to useAsyncData(url, () => $fetch(url)).
  • $fetch: A sort of better fetch(). It should be used on user interaction (e.g., in a addTodo function). It does not provide Network calls duplication.

If you find yourself not being able to use useFetch, you can still wrap the call with useAsyncData to keep the benefits.

Here is an example of data fetching in Nuxt:

<script setup lang="ts">
const { data: products } = await useFetch("/api/v1/products")
</script>

<template>
  <p v-for="product in products" :key="product.id">
    {{ product.name }}, {{ product.price }}$
  </p>
</template>

While this is great, this blocks navigation until the products have been fetched.

<script setup lang="ts">
const {
  data: products,
  status,
  error,
  refresh,
} = useLazyFetch("/api/v1/products")
</script>

<template>
  <div>
    <p v-if="status === 'pending'">Loading products...</p>
    <p v-else-if="error">Error: {{ error.message }}</p>
    <template v-else>
      <p v-for="product in products" :key="product.id">
        {{ product.name }}, {{ product.price }}$
      </p>
    </template>
    <button @click="refresh">Refresh Products</button>
  </div>
</template>

This example uses useLazyFetch to fetch products lazily. This means that the page will render immediately, and the products will be fetched in the background. This pattern is useful for improving the user experience.

Hot Reload

Nuxt.js offers a hot reload feature like Vue.js. Whenever you make changes to your code, Nuxt will automatically apply those changes and reflect them in real-time without requiring a page refresh.

This greatly enhances the development experience by providing instant feedback and speeding up the development process.

State Management

Nuxt provides built-in state management that can simplify your application's data flow. While you can use Pinia for complex state management, Nuxt offers the useState composable for simpler use cases.

The useState composable creates a reactive and SSR-friendly shared state. It's particularly useful for sharing state between components, pages, and server-side code.

<script setup>
const counter = useState("counter", () => 0)

function increment() {
  counter.value++
}
</script>

<template>
  <div>
    Counter: {{ counter }}
    <button @click="increment">Increment</button>
  </div>
</template>

In this example, useState creates a reactive counter that persists across component re-renders and can be shared throughout the application.

Plugins

Nuxt's plugin system allows you to extend core functionality and add global components, functions, or libraries. Plugins are automatically imported and run before instantiating the root Vue application.

To create a plugin, add a .js or .ts file in the /plugins directory:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.provide("test", () => console.log("Hello from plugin!"))
})

You can then use this plugin throughout your application:

<script setup>
const { $test } = useNuxtApp()
$test() // "Hello from plugin!"
</script>

Rendering Modes

Not every application is the same, and Nuxt offers a few options to optimize for your project. Below is everything you can do with Nuxt.

ModeExplanationUsage
SSR (Server Side Rendering)HTML is rendered on the server and sent to the clientThe preferred mode for most applications
SPA (Single Page Application)JavaScript is rendered on the client, providing dynamic and fast navigationPreferable for Dashboards and highly interactive web apps
SSG (Static Site Generation)HTML is generated at build time, creating static filesIdeal for static websites and blogs
ISR (Incremental Static Regeneration)Static pages are built at runtime and can be updated incrementallySuitable for content-heavy sites with frequent updates
SWR (Stale While Revalidate)Data fetching strategy that shows stale data while revalidating in the backgroundUseful for dynamic data fetching in SPAs and SSR apps

Each mode has their own advantages and disadvantages. If you feel like your project could make use of a combination of these modes, you will love the Hybrid Rendering feature.

Hybrid Rendering

Nuxt can be configured to render each route differently. This feature provides granular control over how to render different parts of your application.

export default defineNuxtConfig({
  routeRules: {
    "/": { prerender: true }, // Static Site Generation
    "/products/**": { swr: 3600 }, // SWR with 1 hour cache
    "/admin/**": { ssr: false }, // Client-side rendering
    "/api/**": { cors: true }, // Enable CORS on API routes
    "/blog/**": { static: true }, // Serve as static resource
  },
})

See the full example in the Documentation.

Nitro

Since Nuxt is a full-stack framework (meaning we both have a "frontend" and "backend"), it also needs a server.

The "backend" of Nuxt is powered by the Nitro server engine. Under the hood it uses h3, a minimal Node.js framework.

Key features:

  1. Serverless-ready: Optimized for serverless environments out of the box.
  2. Universal Deploy: Compatible with all major cloud providers.
  3. Standalone: Can be used as a standalone server for your APIs.
  4. File-based routing: Similar to Nuxt, Nitro uses file-based routing.
// server/api/hello.ts
export default defineEventHandler((event) => {
  return {
    message: "Hello World!",
  }
})

This example demonstrates a simple Nitro route that returns a static JSON response.

// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
  try {
    // Fetch all products from the database
    const products = await db.select().from(productsTable)
    return products
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: "Internal Server Error",
    })
  }
})

This more complex example shows how to use Nitro with a database (using Drizzle ORM). It fetches data asynchronously and includes error handling, representing a typical real-world API endpoint.

Nuxt DevTools

Nuxt comes with built-in DevTools that provide powerful debugging and inspection capabilities. These tools offer insights into your application's structure, performance, and runtime behavior.

With Nuxt DevTools, you can:

  • Inspect component hierarchy
  • View and modify state in real-time
  • Analyze network requests
  • Debug server-side rendered content
  • ...and much more!

To use DevTools, run your Nuxt application in development mode, and they will be available in your browser. If you can't see them, make sure to enable them in the nuxt.config.ts:

export default defineNuxtConfig({
  devtools: {
    enabled: true,
  },
})

Wrapping up

Nuxt.js is a powerful and flexible meta-framework for Vue.js that streamlines the process of building modern web applications.

Nuxt combines these features with an intuitive development experience, making it an excellent choice for projects of all sizes.

Whether you're building a small personal website or a large-scale enterprise application, Nuxt provides the tools and structure to help you create performant, SEO-friendly, and maintainable Vue.js applications.