Nuxt3 crash course

Getting started quickly with Nuxt3

Vue Framework work's just fine. But, you probably need the added benefits of a server-side rendered web application (Search Engine Optimization, faster development with autogenerated routes, prerendering etc). Also, Nuxt3 comes with cool new features compared to its predecessor Nuxt2 e.g., auto-imports for your components and composables (functions that hold some functionality); a new server engine (Nitro); improved data fetching (instantaneous i.e., no load time for data); visit nuxt.com for more.

In this article, we will brush through the syntax to get you started quickly with Nuxt3. This will be a breeze if you are familiar with Vue and the composition API.

Installation

Have Node and npm installed on your computer. Run the following command to create a project and navigate to your project folder (replace YOUR_APP_NAME with a name of your choice):

npx nuxi init YOUR_APP_NAME
cd YOUR_APP_NAME

Pages

pages/

Use this folder at the root of your project to create pages for your website. For the home page, add index.vue. If you have a set of related pages, you can nest another folder inside the pages folder. For example, pages/products/index.vue and pages/products/[id].vue. The latter is a dynamic route that changes based on the id property. Also pages/products.vue is similar to pages/products/index.vue.

To make use of dynamic routes, make use of the useRoute() composable function.

<template>
    <!-- Your dynamic content goes here -->
</template>
<script setup>
    const { id } = useRoute().params
</script>

NuxtLink eliminates the need of having traditional anchor tags. NuxtLink works for both internal and external links and adds default attributes (such as noopener and noreferrer, more on this here).

<NuxtLink to="/about">Home</NuxtLink>
<!-- <a href="https://nuxtjs.org" rel="noopener noreferrer">...</a> -->

The advantage of NuxtLink is that it doesn't create a fresh request to the server which enforces "SPA-like" behaviour as it intercepts the request to the server. Furthermore, active CSS classes are applied to NuxtLink automatically (which you can leverage to add custom CSS styling). Doing an inspection on a Nuxt app with internal links (route links), the anchor tag will look like this:

Notice router-link-active and router-link-exact-active?

Layouts

layouts/

Some scenarios in web design implementation require you to have a basic skeletal structure that is common across pages. For example, a navigation bar and footer that cuts across several pages on a website. This is a good candidate for Nuxt Layouts.

<!-- layouts/default.vue, for a default layout included automatically in all your pages -->
<template>
    <nav> <!-- nav content --> </nav>
    <div>
        <slot/>
    </div>
    <footer> <!-- footer content --> </footer>
</template>

For other layout files other than layouts/default.vue, you will need to explicitly include on your pages using definePageMeta() composable function as follows:

<script setup>
definePageMeta({
    layout: 'products'
})
</script>

Nuxt modules

I will use the TailwindCSS module as an example.

npm install --save-dev @nuxtjs/tailwindcss

After installation, add the module using the nuxt.config.ts file as follows:

// nuxt.config.ts
export default defineNuxtConfig({
    modules: ['@nuxtjs/tailwindcss']
})

To extend TailwindCSS with your custom components or utilities (this is optional) add tailwind.css file in the assets folder:

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

/* example styling that you might have */
@font-face {
    font-family: 'AndikaBold';
    src: local("fonts"),
     url(~/assets/fonts/Andika-Bold.ttf) format("truetype");
}

body {
    font-family: 'AndikaBold', san-serif;
}

/* example components that you might have */
@layer components {
    .btn {
        @apply px-3 py-2 m-2 hover:font-bold hover:shadow-md;
    }

    .btn-transparent {
        @apply  rounded-3xl  text-[#000] border-2 border-[#313131] hover:bg-[#313131] hover:text-white; 
    }

}

This approach is strict with the tailwind.css naming. Alternatively, follow this guide which achieves the same result but with an extra step.

Reusable components & Props

components/

For component reusability while taking advantage of Nuxt3 auto-imports.

Pass data to your child component. Make use of defineProps() composable function inside of your child component to capture props passed from a parent component.

<!-- example component file named "ProfileCard.vue" -->
<template>
 <div class="card">
    <img :src="product.img" />
    <!-- other content -->
 <div>
</template>
<script setup>
    const { product } = defineProps(['product'])
</script>

<!-- "About.vue" -->
<template>
<-- other content -->
 <ProfileCard :product = "p" />
</template>
<script setup>
const { data:p } = useFetch('https://example.api')
</script>

Error Page

/error.vue

Place this at the root of your project. Also, note that it is location and name sensitive. Capture the error object using props (i.e defineProps() composable).

<template>
    <p> {{ error.statusCode }} </p>
    <p> {{ error.message }} </p>
    <button @click="handleClearError()">Back home</button>
</template>
<script setup>
 defineProps(['error'])
 const handleClearError = () => clearError({ redirect: '/' })
</script>

clearError() composable function clears the error and navigates to the home page /

You can also create errors as shown in the example below:

//pages/products/[id].vue
<template> <!-- content here --></template>
<script setup>
    if(!product.value) {
        throw createError({
            statusCode: 404,
            statusMessage: 'Product not found',
            fatal: true
        })
    }
</script>

Metadata

You have 3 options when it comes to how to define metadata in your nuxt app.

  1. Using nuxt.config.ts

  2. Using useHead() composable function from inside your Nuxt page

  3. Nuxt metadata-related tags from inside your Nuxt page

Adding metadata in your individual page overrides an existing declaration of metadata from nuxt.config.ts . You can use this to your advantage when you want to modify metadata on certain pages of your website.

Alternative #1

// nuxt.config.ts
export default defineNuxtConfig({
    // other declarations ...,
    app: {
        head: {
            title: 'your title',
            meta: [
                { name: 'description', content: 'example description'         }
            ],
            link: [
                { rel: 'stylesheet', href: 'https://somestylesheecdn.example'}
            ]
        }
    }
})

Alternative #2

// e.g., index.vue
// <template> ... </template>
<script setup>
    useHead({
        title: 'your title',
        meta: [
            { name: 'description',  content: 'example description' }
        ],
        // others ...
    })

Alternative #3 (allows dynamic data)

<template>
    <Head>
        <Title> your title | {{ data.title }} </Title>
        <Meta name="description" :content="data.desc" />
    </Head>
</template>

Custom server routes

server/

If you need to protect sensitive credentials like secret keys from being visible from the browser while making network requests, then Nitro-powered server routes are the way to go. They are structured the same way as your page routes.

Simple server route

// e.g., /server/api/example.js
export default defineEventHandler(() => {
    return {
        message: `Hello, World!`
    }
})

// then you can consume from your page or component as follows:
<script setup>
    const { message:msg } = await useFetch('/api/example')
</script>

Simple server route with query parameters

// e.g., /server/api/example.js
export default defineEventHandler((event) => {
    const { topic } = getQuery(event) // replaces `useQuery`
    return {
        message: `Hello, ${topic}!`
    }
})

// then you can consume from your page or component as follows:
<script setup>
    const { message:msg } = await useFetch('/api/example?topic=nuxt')
</script>

Use event object and getQuery composable to capture the query parameter ("topic" in the above example).

Simple server route for handling POST request

export default defineEventHandler(async (event) => {
    const { topic } = getQuery(event)
    const { level } = await useBody(event)

    return {
        message: `Hello, ${topic}. This is for { level } devs.`
    }
})

// create a POST request from your page or component as follows:
<script setup>
    const { message:msg } = await useFetch('/api/example?topic=nuxt',{
       method: 'post',
       body: { level: 'intermediate' } // no need of JSON.stringify()
    })
</script>

Dynamic Server routes

In this example, we'll demonstrate two concepts, dynamic server routing and environment variables (this is the typical use of server routes in my opinion i.e requests with sensitive content that need not be exposed on the client).

// server/api/currency/[currency_code].js
export default defineEventHandler(async (event) => {
    const { code } = event.context.params // access [currency_code]
    const { currencyKey } = useRuntimeConfig()
    const uri = `https://api.currency.api.com/v3/latest?currencies=${code}&apiKey=${currencyKey}`
    const { data } = await $fetch(uri)

    return data
})

// from your page or component
<script setup>
    const { data } = await useFetch('/api/currency/KES')
</script>

/server/api/currency/[currency_code].js : notice the similarity with dynamic routes covered earlier?

To finish the above example, create .env file at the root of your project / .

// .env
CURRENCY_API_KEY=ae452894fhgb028 // guess work, not a real secret key!

We then expose our environment variable to the app using nuxt.config.ts .

export default defineNuxtConfig({
    runtimeConfig: {
        currencyKey: process.env.CURRENCY_API_KEY, // key only exposed on the server
        public: {
            // keys you want to expose to the client go here ...
        }
    }
})

useAsyncData

// your page or component
<script setup>
    const { data, pending, error, refresh } = await useAsnycData('mountains', () => $fetch('https://api.nuxtjs.dev/mountains'))
</script>

The above example uses a server side fetch ("$" prefixed functions run on the server). The problem is, an extra request is also made on the client side to get data when the component is mounted. To avoid making multiple requests on the same endpoint, wrap $fetch() request with useAsyncData() as demonstrated above.

useAsyncData() is also great on static sites as it prefetches data for all pages and builds a payload. This makes the static site run even faster!

Middleware

middleware/

A middleware is some logic that runs when a route changes. You can use it to implement user authorization or logging on your web app.

// e.g., middleware/auth.ts
export default defineNuxtRouteMiddleware(to => {
    // check if a user is logged in
    // return true
    // or
    return navigate('/') // failure, go back to home page
})

// You can then register the above middleware on your page as follows:
<script setup>
    definePageMeta({
        middleware: ['auth']
    })
</script>

Configure route rules

This gives us control over the type of rendering we may need for specific routes, achieving a "hybrid web app". We do this configuration inside nuxt.config.ts

// nuxt.config.ts
export default defineNuxtConfig({
    nitro: {
        prerender: {
            crawlLinks: true,
            routes: ['/posts']
        }
    },
    routeRules: {
        '/': { prerender: true }, // static site
        '/posts/**': { prerender: true }, // static site
        '/api/example': { cors: true },
        // Incremental static regeneration: update static content instantly
        // without needing a full rebuild of your site
        // https://www.smashingmagazine.com/2021/04/incremental-static-regeneration-nextjs/
        'blog/**': { isr: true },
        // Admin dashboard only renders on the client side
        '/admin/**': { ssr: false }
    }
})

Prerendered content is static content. You can inspect the page headers to see if there is an Age key to confirm that your particular page is indeed rendered as a static site.

Creating global state without 3rd party state manager

composable/

We will utilize a composable for this.

// e.g., composables/my-global-state-feature.ts
export const useMyState = () => useState(() => ({
    count: 0
}))

// Inside your page or component
<script setup>
 const state = useMyState() // ref<{ count: 0 }>
</script>

Note that this is not a clean way of doing things (any composable can mutate this state from anywhere), for more complex state management use Pinia.

Please leave a like or comment if you found this article helpful.