Nuxt 3 Beta: What’s New and How to Get Started

    Ivaylo Gerchev
    Share

    In this article, we’ll explore the most notable new features and improvements offered by Nuxt 3, and we’ll also look at how most of them can be used in practice. This will provide a good overview of what’s possible with Nuxt 3 and how you can implement its goodies in your projects.

    An Overview of Nuxt

    Nuxt is a high-level, open-source application development framework built on top of Vue. Its aim is to speed up, simplify, and facilitate the development of Vue-based apps.

    It does this by providing the following:

    • Most of the best web performance optimizations are incorporated out of the box.
    • Automated app scaffolding and development. Nuxt combines and integrates a curated set of best of its class tools in a form of an optimized and fine-tuned starter project.
    • An opinionated set of directory structure conventions for managing pages and components more easily and efficiently.

    All these goodies makes Nuxt already a perfect choice for building Vue apps. But the good news is that, after a long delay, Nuxt 3 beta version was announced in October, 2021. This completely re-architected version promises to be faster, lighter, more flexible and powerful, offering top-notch DX (Developer Experience). Nuxt is now better than ever, and it brings to the table some really impressive features. Let’s find out what they are.

    What’s New in Nuxt 3 Beta

    Nuxt 3 beta comes with a lot of improvements and exciting new features. Let’s explore the most notable of them.

    Nitro is a new server engine build for Nuxt on top of h3. It provides the following benefits:

    • API routes support. Your server API and middleware are automatically generated by reading the files in server/api/ and server/middleware/ directories respectively. You can create the desired API endpoints just by placing the corresponding files in the server/api/ directory. For example, a tasks.js file will generate an http://yourwebsite.com/api/tasks endpoint. Functions in server/middleware/ load automatically and run in every request — which is much similar than how Express works.
    • Apps can be deployed to a variety of serverless platforms such as Vercel, Netlify, AWS, Azure, etc. Plus, some platforms (Vercel, Netlify) are automatically detected when deploying, without the need to add custom configuration.
    • The built app can be deployed on any JavaScript supporting system including Node, Deno, Serverless, Workers, etc.
    • Incremental Static Generation. This allows for using a hybrid mode for static plus serverless sites. The end result is a mix of SSR (server-side rendering) and SSG (static site generation). (This is a planned feature, but it’s not implemented yet.)
    • Much lighter app output. The built app is put into a universal .output/ directory. The build is minified and any Node modules (except polyfills) are removed. This strategy targets modern browsers and it produces up to 75x smaller bundles, both on client and server.
    • Optimized cold start with dynamic server code-splitting and async-loaded chunks.
    • Faster bundling and hot reloading.

    Nuxi is a new Nuxt CLI. It provides a zero-dependency experience for easy scaffolding new projects and module integration.

    Nuxt Kit provides a new flexible module development experience with TypeScript support and cross-version compatibility.

    Nuxt Bridge allows you to use some of the Nuxt 3 features in your existing Nuxt 2 projects. Its aim is to make future migration smoother by offering to Nuxt 2 users the ability to incrementally update/upgrade their projects. Here are the Nuxt 3 features which you can include in your Nuxt 2 project, as they are stated on Nuxt’s website:

    • Using Nitro server with Nuxt 2
    • Using Composition API (same as Nuxt 3) with Nuxt 2
    • Using new CLI and devtools with Nuxt 2
    • Progressively upgrade to Nuxt 3
    • Compatibility with Nuxt 2 module ecosystem
    • Upgrade piece by piece (Nitro, Composition API, Nuxt Kit)

    Nuxt Bridge also aims to facilitate the upgrades for the whole Nuxt ecosystem. For that reason, legacy plugins and modules will keep working, the config file from Nuxt 2 will be compatible with Nuxt 3, and some Nuxt 3 APIs (like Pages) will remain unchanged.

    These were the so-called “big” features, but Nuxt 3 comes with lots more small features and improvements. We’ll explore them in the following list:

    • Vue 3 support. Nuxt 3 version is aligned with Vue 3 so you can leverage all the great features of Vue 3 such as Composition API, composables, and more. Nuxt already offers some of its functionality in a form of built-in composables like useFetch(), useState(), and useMeta(). For more information about the Vue 3 Composition API, see How to Create Reusable Components with the Vue 3 Composition API.
    • Webpack 5 and Vite support. Enjoy the latest versions of the best bundlers offering faster build times and smaller bundle size, with no configuration required. Vite, as its name suggests, offers super fast HMR (hot module replacement).
    • TypeScript support with type checking, better autocompletion and error detection, and auto-generated types. If you don’t like or need TypeScript, you still can use Nuxt without it.
    • Native ESM Support.
    • Suspense support which allows you to fetch data in any component, before or after navigation.
    • Auto-import for global utilities and composable functions. Inside a <script setup> or setup() function you can use any of the composable functions that Nuxt 3 offers, such as useFetch(), useState(), useMeta(), and also Vue reactivity functions such as ref(), reactive(), computed(), etc. In the new composables/ directory you can define all your functionality in composition functions, which are auto-imported as well. This is true even for the composables from the VueUse library, after a small configuration.
    • Optional Pages support. Vue Router 4 is used only if you have created a pages/ directory. This can produce lighter builds if you don’t use pages.
    • Nuxt Devtools, which offers seamlessly integrated debugging tools right from the browser. (This is a planned feature, but it’s not implemented yet.)

    Okay, now that we’ve seen how great Nuxt is in its latest implementation, let’s see how we can use its super powers in action.

    In the following sections, we’ll explore how to get started with Nuxt 3 and how to use it to implement some minimal blog functionality. Particularly, we’ll examine the following:

    • creating a fresh Nuxt 3 project
    • adding Tailwind CSS support to the project
    • creating and using custom layouts
    • creating blog pages
    • creating and using custom components
    • using the Nuxt 3 built-in composables
    • creating and using custom composables

    Getting Started With Nuxt 3

    Note: before we begin, please make sure you have Node v14 or v16 installed on your machine.

    We’ll start by creating a fresh Nuxt 3 project. To do so, run the following command in your terminal:

    npx nuxi init nuxt3-blog

    This will set up a new project for you without any dependencies installed, so you need to run the following commands to navigate to the project and install the dependencies:

    cd nuxt3-blog
    npm install

    And finally, to start the dev server, run this command:

    npm run dev

    Open http://localhost:3000 in your browser. If everything works as expected, you should see the following welcome page.

    Nuxt starter project home page

    If you’re familiar with Nuxt 2, you’ll probably notice that the project structure in Nuxt 3 has been a bit simplified.

    Project folder structure

    Here’s a short list exploring the most notable changes in the project structure in Nuxt 3 compared to Nuxt 2. In Nuxt 3:

    • An app.vue file is added. It’s the main component in your application. Whatever you put in it (CSS, JS, etc.) will be globally available and included in every page.
    • The use of the pages/ directory is optional. You can build your app only with app.vue as a main component and other components placed in the components/ folder. If that’s the case, vue-router won’t be used and the app’s build will be much lighter.
    • A new composables/ directory is added. Each composable added here is auto-imported so you can use it directly in your application.
    • A new .output/ directory is added, as we mentioned before, producing smaller bundles.

    Building a Minimal Blog With Nuxt 3

    Note: you can explore the complete source code for this project in the Nuxt 3 Blog Example repo.

    In this section, we’ll explore the basics of Nuxt 3 by building a super minimalist blog. We’ll need a bit of styling and Tailwind CSS is a great choice for that.

    Including Tailwind CSS in the project

    To install Tailwind and its peer-dependencies, run the following:

    npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

    Now we need to generate Tailwind and PostCSS configuration files. Run the following:

    npx tailwindcss init -p

    This will generate tailwind.config.js and postcss.config.js files in the root directory. Open the first one and configure the content option to include all of your project’s files that contain Tailwind utility classes:

    // tailwind.config.js
    module.exports = {
      content: [
        "./components/**/*.{vue,js,ts}",
        "./layouts/**/*.vue",
        "./pages/**/*.vue",
        "./plugins/**/*.{js,ts}",
        "./app.vue",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    }

    Note: from version 3, Tailwind no longer uses PurgeCSS under the hood and the purge option has been renamed to content. Please read the Content Configuration section of the Tailwind docs for more information about the content option.

    The postcss.config.js file doesn’t need any configuration. It already has Tailwind and Autoprefixer included as plugins:

    // postcss.config.js
    module.exports = {
      plugins: {
        tailwindcss: {},
        autoprefixer: {},
      },
    }

    The next step is to add Tailwind’s styles. Create a new assets directory and put a css folder in it. In the assets/css/ directory, create a styles.css file and put the following content in it:

    /* assets/css/styles.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;

    The final step is to update nuxt.config.ts with the following content:

    // nuxt.config.ts
    import { defineNuxtConfig } from 'nuxt3'
    
    // https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
    export default defineNuxtConfig({
      css: ["@/assets/css/styles.css"],
      build: {
        postcss: {
          postcssOptions: require("./postcss.config.js"),
        },
      },
    })

    And now we can use all Tailwind’s utilities in our project.

    Creating the blog’s layout

    We’ll start creating our blog with a simple layout containing a header and a footer. Create a layouts directory and add a blog.vue file in it with the following content:

    <!-- layouts/blog.vue -->
    <template>
      <div>
        <header class="text-white bg-green-500 p-4">HEADER</header>
        <slot />
        <footer class="text-white bg-green-500 p-4">FOOTER</footer>
      </div>
    </template>

    When we use this layout, the <header> and <footer> elements will be added as they appear here. The <slot /> component defines where the actual content (which will vary depending on where you’ve used the layout) will be loaded. Now we can reuse this layout in every page or component just by providing the name of our custom layout following this pattern:

    <script>
      export default {
        layout: [custom_layout_name]
      }
    </script>

    We’ll see this in action in the next section.

    Creating the blog’s pages

    We already have a blog layout, and it’s time to create blog’s pages which will make use of it. Before we do that, we need to make a small correction in the app.vue file. Open it and replace the <NuxtWelcome /> component with <NuxtPage />:

    <!-- app.vue -->
    <template>
      <div>
        <NuxtPage />
      </div>
    </template>

    This tells Nuxt that we’re going to use pages in our application.

    Let’s create the home page, which will list the blog’s posts. Create a new pages directory and put an index.vue file in it with the following content:

    <!-- pages/index.vue -->
    <script>
      export default {
        layout: "blog"
      }
    </script>
    
    <script setup>
      const { data: posts } = await useFetch('https://jsonplaceholder.typicode.com/posts')
    </script>
    
    <template>
      <div>
        <article class="m-4 md:w-1/2 lg:w-1/3" v-for="post in posts" :key="post.id">
          <NuxtLink :to="`/post-${post.id}`">
            <h2 class="mb-2 capitalize text-2xl font-semibold">{{ post.title }}</h2>
          </NuxtLink>
          <p>{{ post.body }}...</p>
        </article>
      </div>
    </template>

    Here, we first use a regular <script> element to define that we want to use our custom blog layout. Then we use the script setup> syntactic sugar to create a setup function. Then we use the Nuxt 3 useFetch() composable to fetch the posts. In the template section, we iterate over the posts and create an <article> element for each one. We use the <NuxtLink> element to create a link to each single post. Each post will have a URL following the post-[id] pattern.

    The job is half done now. What’s left is to create a page representing a single post. Nuxt 3 offers a new brackets syntax for the pages/ directory, so we can make parts of a page’s name dynamic. Let’s test it.

    Create a post-[id].vue page with the following content:

    <!-- pages/post-[id].vue -->
    <script>
      export default {
        layout: "blog"
      }
    </script>
    
    <script setup>
      const route = useRoute()
      const { data: post } = await useFetch(`https://jsonplaceholder.typicode.com/posts/${route.params.id}`, { pick: ['title', 'body'] })
    </script>
    
    <template>
      <div>
        <NuxtLink to="/">
          <h1 class="m-4 hover:underline">Back to Home</h1>
        </NuxtLink>
        <!-- A <quote /> component should be added here later on -->
        <article class="m-4 md:w-1/2 lg:w-1/3">
          <h2 class="mb-2 capitalize text-2xl font-semibold">{{ post.title }}</h2>
          <p>{{ post.body }}</p>
        </article>
      </div>
    </template>

    Here, we add the blog layout again. Then we use the useRoute() composable to get the id parameter of the current URL, which we need in the useFetch(). We also use the pick option, so we’ll fetch only what we need (the title and the body properties). Then, in the template section, we use the post variable to render the post’s title and body. We also use the <NuxtLink> component to create a link to the home page.

    Great. Now if you run the project (if the server is already running you might need to restart it) you should see a list of posts in the home page.

    Blog Posts List

    And when you click on a post’s title, you’ll be redirected to the single post page displaying the post with the corresponding ID.

    Blog Single Post

    Creating a quote component

    Apart from pages, components are the most used elements in a Nuxt application. Let’s see how we can create a simple quote component that will render a random quote of the day for each visited post.

    Create a new components folder and put a quote.vue file in it with the following content:

    <!-- components/quote.vue -->
    <script setup>
      const quote = ref('')
      const { data: qotd } = await useFetch('https://favqs.com/api/qotd')
      quote.value = qotd.value.quote
    </script>
    
    <template>
      <div class="p-4 md:w-1/2 lg:w-1/3">
        <p class="p-2 font-bold bg-blue-400 text-white">Quote of the Day</p>
        <div class="p-2 bg-blue-100  text-indigo-600">
          <p class="italic">{{ quote.body }}</p> 
          <p class="mt-2 italic text-sm font-medium">- {{ quote.author }}</p>
        </div>
      </div>
    </template>

    Here, we fetch a random quote of the day and assign it to the quote variable. We then use it in the template to render the body and author of the quote. To test our quote component, we need to include it inside the post-[id].vue file right above the <article> element:

    <!-- pages/post-[id].vue -->
    ...
    <template>
      <div>
        <NuxtLink to="/">
          <h1 class="m-4 hover:underline">Back to Home</h1>
        </NuxtLink>
        <quote /> <!-- put quote component here -->
        <article class="m-4 md:w-1/2 lg:w-1/3">
          <h1 class="mb-2 capitalize text-2xl font-semibold">{{ post.title }}</h1>
          <p>{{ post.body }}</p>
        </article>
      </div>
    </template>

    Now, when you open a particular post, a quote-of-the-day box should appear above the post.

    Blog Post with Quote Component

    Creating and using the useCounter() composable

    The last thing we’ll explore is how to create and use a composable. We’ll use the famous counter example for this exercise. Create a new composables folder and put a useCounter.js file in it with the following content:

    // composables/useCounter.js
    export default () => {
      const counter = ref(0)
      const increment = () => counter.value++
      const decrement = () => counter.value--
      const counterDouble = computed(
        () => counter.value * 2
      )
      return {
        counter,
        increment,
        decrement,
        counterDouble
      }
    }

    In this composable, we add a counter reactive property with value set to zero and a computed property that doubles the counter’s value. We also add two functions to increment and decrement the counter’s value. Then we return all properties and functions so they can be available for use.

    Now, to test it, let’s create another page named counter.vue with the following content:

    <!-- pages/counter.vue -->
    <script>
      export default {
        layout: "blog"
      }
    </script>
    
    <script setup>
      const { counter, increment, decrement, counterDouble } = useCounter()
    </script>
    
    <template>
      <div>
        <p class="m-2 text-3xl"><span class="font-semibold">Counter:</span> {{ counter }} x 2 = {{ counterDouble }}</p>
        <button @click="increment" class="m-2 py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700">+ Increment</button>
        <button @click="decrement" class="m-2 py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700">- Decrement</button>
      </div>
    </template>

    We can see here that we use useCounter() directly because it’s auto-imported by Nuxt. Then we use its variables and functions in the template.

    Restart the server and open http://localhost:3000/counter. You should see the counter as it’s shown in the image below.

    Blog Counter Page

    Conclusion

    In this tutorial, we explored the most notable new Nuxt 3 features and improvements, and demonstrated how most of them can be used in practice. I hope this has given to you a good overview of what’s possible with Nuxt 3 and how you can implement its goodies in your projects. Lastly, I must warn you that Nuxt 3 is still in beta, which means that it might not be production-ready yet. Of course, this shouldn’t stop us from learning and experimenting with it, right? So let’s play!