Nuxt – The Intuitive Web Framework

Nuxt – The Intuitive Web Framework

Introduction

Nuxt is an open-source JavaScript framework based on Vue.js. The latest version is currently Nuxt 3. Nuxt 3 is an upgrade from the Nuxt framework based on Vite, Vue 3, and Nitro with support from Typescript.

Nuxt simplifies the development of web applications under Server-side rendering (SSR) and Static Site Generation (SSG) methods, in addition to other methods such as CSR, ISR, ESR, and SWR. Nuxt provides a structured and convention-based approach to building Vue.js applications, allowing developers to focus on developing features instead of dealing with complex configurations.

 

Why use Nuxt?

Server-side Rendering (SSR): Nuxt enables server-side rendering, which means that the Vue.js components are pre-rendered on the server before being sent to the client. This improves initial page load performance and allows for better search engine optimization (SEO), as search engines can index fully rendered content.

Static Site Generation (SSG): Nuxt can generate static HTML files at build time, which can be served directly from a CDN or static hosting service. This approach eliminates the need for server-side rendering on each request, resulting in faster page loads and improved scalability.

Faster rendering: The Vue Virtual DOM (VDOM) has been rewritten from the ground up, allowing for better rendering performance. On top of that, when working with compiled Single-File Components, the Vue compiler can further optimize them at build time by separating static and dynamic markup. This results in faster first rendering (component creation) and updates, and less memory usage. In Nuxt 3, it enables faster server-side rendering as well.

Smaller bundle: With Vue 3 and Nuxt 3, a focus has been put on bundle size reduction. By implementing the tree-shaking technique, the production environment of the application will exclude Vue’s features such as template directives and components that are not used when bundling. This can help reduce file size, download faster, and improve loading time for applications. This way, a minimum Vue 3 application can be reduced to 12 kb gzip compressed.

Extensive ecosystem: Nuxt has a diverse development ecosystem contributed by the community. You can take advantage of community plugins, modules, and tools to extend and customize your application.

Support Typescript: Although Nuxt 2 already supports Typescript, Nuxt 3 has improved Typescript support, you can use Type-checking and other tools that Nuxt provides based on Typescript.

Simplified development: With Convention Over Configuration, Nuxt reduces boilerplate code and simplifies the development process. It comes with built-in features such as routing, code splitting, and state management, enabling you to focus on building your application logic rather than worrying about setup and configuration.

Better experience for developers: Nuxt offers a robust development environment with features like hot module replacement, automatic reloading, error handling, and more innovative tools like debugging, testing, dev tools, and others. These features streamline the development workflow, allowing for faster iterations and better debugging capabilities.

Scalability and performance: Nuxt optimizes your application’s performance through features like automatic code splitting, lazy loading, and pre-rendering. It ensures that only the necessary JavaScript is loaded for each page, resulting in faster load times and a better user experience. Additionally, Nuxt’s modular architecture allows for easy scalability when upgrading your application to higher versions.

 

Some built-in supported technologies

  1. Webpack 5
  2. Vite
  3. Nitro
  4. Vue 3
  5. Router 4
  6. Typescript

 

Feature comparison

In the table below, there is a quick comparison between 3 versions of Nuxt:

 

Directory structure

.nuxt: Nuxt uses the “.nuxt/” directory during development to create your Vue application.

.output: Nuxt creates the “.output/” directory when building your application for the production environment.

assets: The “assets/” directory is used to add all the content of the site that the builder (Webpack or Vite) will process. The folder usually contains the following file types: Stylesheets (CSS, SASS, etc.), Fonts, and images will not be served from the “public/” directory.

components: The “components/” directory is where you put all your Vue components which can then be imported inside your pages or other components.

composables: Nuxt 3 uses the “composables/” directory to automatically import your Vue composables into your application using auto-imports.

content: Supports .md, .yml, and .csv files to create a CMS for your application.

layouts: Used to change the look and feel of your application. An application can have multiple layouts for example: admin layout, guest layout, and registered clients layout. These layouts will be reused on different pages to handle their appearance (sidebar, menu, footer, etc.). During installation, Nuxt CLI provides default layouts/default.vue layout and is used on all pages.

middleware: Used to handle requests before the page is displayed to the user. Middleware allows you to perform tasks such as authentication, access checking, activity logging, and other custom processing before the page is rendered.

modules: Nuxt scans the “modules/” directory and loads them before starting. It is a good place to place any local modules you develop while building your application.

node_modules: The package manager (npm or yarn or pnpm) creates the “node_modules/” directory to store the dependencies of your project.

pages: Nuxt provides file-based routing to create routes in your web application using Vue Router.

plugins: Nuxt automatically reads the files in your “plugins” directory and loads them at the creation of the Vue application. You can use the .server or .client suffix in the file name to load a plugin only on the server or client side.

public: The “public/” directory contains static files and provides resources directly to HTTP requests without additional processing logic. For example, you can place image files, font files, robot.txt, favicon.ico, or other static files.

server: Nuxt automatically scans files inside these directories to register API and server handlers with HMR support: ~/server/api, ~/server/routes, ~/server/middleware.

utils: Nuxt 3 uses the “utils/” directory to automatically import helper functions and other utilities throughout your application using auto-imports.

.env: Nuxt CLI has built-in dotenv support in development mode and when running nuxi build and nuxi generate. In addition to any process environment variables, if you have a .env file in your project root directory, it will be automatically loaded at build, dev, and generate time, and any environment variables set there will be accessible within your nuxt.config file and modules.

.gitignore: This will list the names of files and folders in the project that you do not want to be parsed every time you manipulate git.

.nuxtignore: The .nuxtigore file allows Nuxt to ignore layouts, pages, components, composables, and middleware in the project root directory (rootDir) during the build process. The .nuxtigore file follows the same specifications as the .gitignore and .eslintigore files, where each line is a glob pattern indicating which files should be ignored.

app.config.ts: Used to configure and customize the Nuxt application.

app.vue: The main component in Nuxt 3 applications.

nuxt.config.ts: The nuxt.config.ts file contains your custom Nuxt configuration and allows you to configure your application, these configurations include head title and associated styles and scripts, middlewares, plugins, authentication, modules, and even APIs.

package.json: Contains all the dependencies and scripts for your application.

tsconfig.json: Nuxt automatically creates a .nuxt/tsconfig.json file with other reasonable defaults. You just need to create a tsconfig.json file outside of the root and import it from the initialized Nuxt file.

 

Feature

Auto-imports

Auto-import is a new feature developed by Nuxt 3 which is quite convenient. All files in components, composables, utils are automatically recognized and just need to name the component, composables, etc. without having to import again. Auto import is enabled by default but can also be turned off in the nuxt.config.ts file as follows:

export default defineNuxtConfig({
  imports: {
    autoImport: false
  }
})

 

Rendering Modes

Nuxt supports different rendering modes, universal rendering, and client-side rendering but also offers hybrid rendering and the possibility to render your application on CDN Edge Servers.

 

Server Engine

Nuxt 3 is powered by a new server engine, Nitro.

  1. Cross-platform support for Node.js, Browsers, service workers, and more.
  2. Serverless support out-of-the-box.
  3. API routes support.
  4. Automatic code-splitting and async-loaded chunks.
  5. Hybrid mode for static + serverless sites.
  6. Development server with hot module reloading.

 

Getting started with Nuxt

Prerequisites: Nodejs version 16.0.0 or later.

A simple setup with Nuxt CLI, used with the nuxi command will help you to install and manage all Nuxt components. With npx installed, you can easily create a project with the command below:

> npx nuxi init project-name

As an output you will get this.

Nuxt project is created with a v3 template. Next steps:
› cd project-name
› Install dependencies with npm install or yarn install or pnpm install
› Start development server with npm run dev or yarn dev or pnpm run dev

Install dependencies.

yarn install

And here is the new project structure.


You should now be able to start your Nuxt application in development mode.

yarn dev -o

Then visit the browser at http://localhost:3000, to see the newly created Nuxt application running.

By default, the app .vue file is the core component that acts as the entry point and renders its content for each application route. Is the root component of your Nuxt application and represents the layout and structure that wraps all other components. It usually contains the main layout, navigation, and any global components or logic that need to be shared across multiple pages.

Change a bit the content in the app.vue file and see the display.

> app.vue
<template>
  <div>
    <h1>Welcome to the homepage</h1>
  <div>
</template>

 

Features

Pages

This is where your Application Views and routes will be located. By default, Nuxt uses a file-based routing system, where each .vue file in the “/pages” directory corresponds to a route.

To use pages, create the file pages/index.vue and add the <NuxtPage /> component to app.vue (or remove app.vue when you want pages/index.vue for default entry). Pages are Vue components and can have any valid extension that Nuxt supports (by default, .vue, .js, .jsx, .mjs, .ts, or .tsx). Nuxt will automatically generate routes for every page in your “~/pages/” directory.

> app.vue (app.vue default entry)
<template>
  <NuxtPage />
</template>

--- .vue ---
> pages/index.vue
<template>
  <p>Home Page</p>
</template>

> pages/about.vue
<template>
  <p>About Page</p>
</template>

--- .ts ---
> pages/index.ts
export default defineComponent({
  render () {
    return h('h1', 'Home page');
  }
});

--- .tsx ---
export default defineComponent({
  render () {
    return <h1>Home page</h1>;
  }
});

 

Layouts

Layout plays an important role in determining the overall structure and design of the pages in your application. Layouts in Nuxt represent a reusable template that wraps around your page components and provides a consistent look and feel across multiple pages. Can create many different layouts like login layout or homepage layout or many other layouts.

If you only have a single layout in your application, we recommend using app.vue with the <NuxtPage /> component instead.

If nothing is set, Nuxt will specify default .vue as the default.

> app.vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

> layouts/default.vue
<template>
  <div>
    <h1>This our default layout</h1>
    <slot />
  </div>
</template>

> pages/index.vue
<template>
  <p>Home Page</p>
</template>

 

Now let’s do something more interesting. Let’s create a custom layout and display our /About page using our customized layout.

> layouts/custom.vue
<template>
  <div>
    <h1>This our custom layout</h1>
    <slot />
  </div>
</template>

> pages/about.vue
<template>
  <p>About Page</p>
</template>

<script setup>
definePageMeta({
  layout: 'custom',
});
</script>

 

Routing

Dynamic routes

Now, let’s look at the different ways to create routes using routing based on Nuxt files.

 

In our example, let’s look at the “pages/” directory structure. There are two different types of pages: static pages and dynamic pages whose content changes based on route parameters.

  1. Static page
    • index.vue → /
    • about.vue → /about
    • category/index.vue → /category
  2. Dynamic page
    • category/[slug].vue → /category/language
    • user-[group]/[id].vue → /user-admin/123
> pages/category/[slug].vue
<template>
  <p>Category slug : {{ $route.params.slug  }}</p>
</template>

<script setup>
const route = useRoute();
useHead({
  title: `Category ${route.params.slug}`
});
</script>

 

> pages/user-[group]/[id].vue
<template>
  <p>Group : {{ $route.params.group }} - ID : {{ $route.params.id }}</p>
</template>

<script setup>
const route = useRoute();
useHead({
  title: `User group ${route.params.group} ID ${route.params.id}`
});
</script>

 

Nested Routes

A hierarchical display page structure where a parent route has child routes or sub-routes. This is useful when you have pages that share a common layout. It is also useful when you want to organize nested routes in your application. Can display nested routes with <NuxtPage>.

 

This file tree will generate these routes:

[
  {
    path: '/user',
    component: '~/pages/user.vue',
    name: 'user',
    children: [
      {
        path: '/',
        component: '~/pages/user/index.vue',
        name: 'user-child'
      },
      {
        path: '/:id',
        component: '~/pages/user/[id].vue',
        name: 'user-id',
	children: [
	   {
	     path: '/profile',
	     component: '~/pages/user/[id]/profile.vue',
	     name: 'user-id-profile'
	   },
	   {
	     path: '/division',
	     component: '~/pages/user/[id]/division/index.vue',
	     name: 'user-id-division',
	     children: [
		{
		  path: '/:division/:divisionId',
		  component: '~/pages/user/[id]/division/[division]/[divisionId].vue',
		  name: 'user-id-division-id'
		}
	     ]
	   },
	 ]
      }
    ]
  }
]

Result from user route with child routes.

> pages/user.vue
<template>
  <p>User Page</p>
  <NuxtPage />
</template>

> pages/user/index.vue
<template>
  <p>User Child Page</p>
</template>

 

Result from user profile route with the parameter user id.

> pages/user.vue (parent route)

> pages/user/[id]/profile
<template>
  <p>User Profile Page - User ID : {{ $route.params.id }}</p>
</template>

 

Result from user division route with the parameter user id.

> pages/user.vue (parent route)

> pages/user/[id]/division/index.vue
<template>
  <p>User ID : {{ $route.params.id }}</p>
  <p>User division : {{ $route.params.division }}</p>
</template>

 

Result from user division route with parameter user id and division id.

> pages/user.vue (parent route)

> pages/user/[id]/division/[division]/[divisionId].vue
<template>
  <p>User ID : {{ $route.params.id }}</p>
  <p>User division : {{ $route.params.division }} - Division ID : {{ $route.params.divisionId }}</p>
</template>

 

Navigation

It is the process of moving between different pages or routes in your application. Nuxt uses the <NuxtLink> component to link pages. It will render a <a> tag with the href attribute to set the route for the page.

> pages/index.vue
<template>
  <div>
    <p>Home Page</p>
    <nav>
      <ul>
        <li><NuxtLink to="/about">About</NuxtLink></li>
        <li><NuxtLink to="/category">Category</NuxtLink></li>
        <li><NuxtLink to="/user">User</NuxtLink></li>
      </ul>
    </nav>
  </div>
</template>

<style scoped>
a {
  text-decoration: none;
  line-height: 1.5em;
  color: #0e0d0d;
  font-weight: 600;
}
</style>

When clicking on each link About, Category, the User will be redirected to the corresponding page.

 

Use <NuxtLink> component with parameter Props “to” have 2 objects of interest:

  1. name: The page name is also the corresponding route. Ex: pages/category/[slug].vue → /category/:slug. Name the appropriate separator between the child folders as “-“: category-slug.
  2. params: Parameters to pass when using dynamic routes.
> pages/category/index.vue
<template>
  <p>Category Page</p>
  <nav>
    <ul v-for="(cate, index) in categories" :key="index">
      <li>
        <NuxtLink :to="{ name: 'category-slug', params: { slug: cate } }">
          {{ cate }}
        </NuxtLink>
      </li>
    </ul>
  </nav>
</template>

<script setup>
const categories = ['language', 'reading', 'programming', 'art', 'other'];
</script>

<style scoped>
a {
  text-decoration: none;
  line-height: 1.5em;
  color: #0e0d0d;
  font-weight: 600;
}
</style>

 

Assets

  1. The “public/” directory content is served at the server root as-is.
  2. The “assets/” directory contains by convention every asset that you want the build tool (Vite or Webpack) to process.

Public directory

For example, referencing an image file in the “public/img/” directory, available at the static URL /img/nuxt.png:

<template>
  <img src="/img/nuxt.png" alt="Discover Nuxt 3" />
</template>

Assets directory

Nuxt won’t serve files in the assets/ directory at a static URL like /assets/my-file.png. If you need a static URL, use the public/ directory.

You can set stylesheet files (CSS, SASS, etc), fonts, or SVG. To insert global statements into your Nuxt components, you can use the Vite option in your nuxt.config file.

See the example below:

> assets/css/styles.css
a {
  text-decoration: none;
  line-height: 1.5em;
  color: #0e0d0d;
  font-weight: 600;
}

> assets/sass/_colors.scss
$primary: #49240F;
$secondary: #E4A79D;

> assets/sass/app.scss
@import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,600;1,700;1,800;1,900&display=swap");
body {
  font-family: 'Nunito',
}
.btn-bg-color {
  background-color: $primary;
}

> nuxt.config.ts
export default defineNuxtConfig({
  css: [
    '~/assets/css/styles.css',
    '~/assets/sass/app.scss',
  ],
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@use "@/assets/sass/_colors.scss" as *;'
        }
      }
    }
  }
})

 

Components

The “components/” directory is where you put all your Vue components which can then be imported inside your pages or other components.
Nuxt automatically imports any components in your “components/” directory (along with components registered by any modules you may be using).

In addition, Nuxt also has built-in components such as: <ClientOnly>, <NuxtPage>, <NuxtLayout>, <NuxtLink>, <Teleport>, etc.

> components
--| BaseHeader.vue
--| BaseFooter.vue
--| base/
----| html/
------| Alert.vue

> layouts/default.vue
<template>
  <div>
    <BaseHeader />
    <BaseHtmlAlert />
    <slot />
    <BaseFooter />
  </div>
</template>

 

Composables

Nuxt provides several composables like useAppConfig, useAsyncData, useCookie, useError, useFetch, useHead, etc. Also, Nuxt will automatically import files inside your “composables” directory with the auto-imports feature.

 

Plugins 

Nuxt automatically reads the files in your “plugins” directory and loads them at the creation of the Vue application. You can use the .server or .client suffix in the file name to load a plugin only on the server or client side.

All plugins in your plugins/ directory are auto-registered, so you should not add them to your nuxt.config separately.

 

Modules

Nuxt provides a list of community-developed modules. Gives you many choices for application development.

 

Install the necessary modules for your application and configure them in the nuxt.config.ts file.

export default defineNuxtConfig({
  modules: [
    '@vueuse/nuxt',
    '@element-plus/nuxt',
    'nuxt-lodash',
    'nuxt-security',
  ]
})

For custom files, Nuxt scans the “modules/” directory and loads them before starting. It is a good place to place any local modules you develop while building your application.

modules/*/*.ts
modules/*.ts

 

Data Fetching

With Nuxt 3, there is almost full support for composables and built-in libraries to perform data-fetching from a browser or server environment including useFetch, useLazyFetch, useAsyncData, useLazyAsyncData, and $fetch.

useFetch

useFetch is rendered on the server side (in Nuxt’s server-side rendering mode) and is used to load data into your Nuxt application. useFetch can be used in pages, components, or plugins. Usage of useFetch is very simple as below:

<template>
  <div>
    Page visits: {{ count }}
  </div>
</template>

<script setup>
const { data: count } = await useFetch('/api/count');
</script>

Options:

  1. method: Request method.
  2. query: Add queries to requests.
  3. params: Alias to query.
  4. body: Request body.
  5. headers: Request headers.
  6. baseURL: Base URL.
  7. key: Unique value, controls how data is cached and how requests are implemented.
  8. server: Whether to fetch the data on the server (defaults to true).
  9. default: Set default values for the data.
  10. pick: Extract specific properties from object data.
  11. watch: Watch changes of some object to trigger a useFetch call.
  12. transform: Use to transform the output.
  13. immediate: When set to false, prevents the request from triggering useFetch immediately.

Return value:

  1. data: The data that useFetch calls from the API.
  2. pending: The useFetch state is still called (true/false).
  3. refresh/execute: The refresh or execute function can be called outside of the useFetch used to fetch the data.
  4. error: Object error in case the API call fails.

Here is a small example using some of the above options.

const { data, pending, error, refresh } = await useFetch('https://api.nuxtjs.dev/mountains',{
    pick: ['title'],
    query: { param1, param2: 'value2' }
})
const refreshData = () => refresh();

This usage will return the title of the elements in the array returned from the URL as “https://api.nuxtjs.dev/mountains?param1=value1&param2=value2” and we can call useFetch again via the refreshData function.

 

useAsyncData

With useFetch, each use passes a long URL into useFetch and if this URL is used in many places it is quite difficult to manage. Usually, we will create an API directory and write API calls to the files in it, then import them into the component or page. With this approach, useFetch cannot be used. For convenience, useAsyncData is similar to useFetch.

<template>
  <div>
     Page visits: {{ data }}
  </div>
</template>

<script setup>
	const { data } = await useAsyncData('count', () => $fetch('/api/count'));
</script>

Suppose there is a function:

export const getPosts = () => axios.get('/api/posts');

To use useAsyncData, we just need to change it to:

const { data } = await useAsyncData('projectSearch', () => getPosts());

UseAsyncData options and return values are the same as in useFetch. There is a small note when using useAsyncData or useFetch that we remember to add keys to them and remember to clearNuxtData if we want useAsyncData or useFetch to be called again when we switch pages. For example, this:

<script setup>
	await clearNuxtData('count');
	const { data } = await useAsyncData('count', () => $fetch('/api/count'));
</script>

 

useLazyFetch

The spelling of useLazyFetch is similar to useFetch because it is basically useFetch with the option lazy: true. Lazy will load data asynchronously, and Nuxt will load data in parallel with the code right behind. Except for useFetch, it will be done before executing the code behind it. In addition, you can use the pending property to determine if the data has been loaded (pending = true when the data has not been loaded and opposite).

<template>
  <div>
     Page visits: {{ count }}
  </div>
</template>

<script setup>
    const { pending, data: count } = await useLazyFetch('/api/count');
</script>

 

useLazyAsyncData

useLazyAsyncData is similar to useAsyncData but with the lazy: true option. The implementation is the same as useAsyncData:

const { pending, data: post } = await useLazyAsyncData('post', () => getPosts());

 

$fetch

Nuxt uses ofetch for global visibility with the $fetch helper to make HTTP requests in your Vue app or API routes.

We should use useFetch or useAsyncData + $fetch to prevent double fetch when fetching component data.

<script setup>
// During SSR data is fetched twice, once on the server and once on the client.
const dataTwice = await $fetch('/api/item')
// During SSR data is fetched only on the server side and transferred to the client.
const { data } = await useAsyncData('item', () => $fetch('/api/item'))
// You can also useFetch as shortcut of useAsyncData + $fetch
const { data } = await useFetch('/api/item')
</script>

You can use $fetch for any method executed on the client-side.

<template>
  <button @click="contactForm">Contact</button>
</template>

<script setup>
function contactForm() {
  $fetch('/api/contact', {
    method: 'POST',
    body: { hello: 'world '}
  })
}
</script>

 

Server

Nuxt automatically scans files inside these directories to register API and server handlers with HMR support: ~/server/api, ~/server/routes, ~/server/middleware.

Ex: create a route /api/hello with /server/api/hello.ts.

> server/api/hello.ts
export default defineEventHandler((event) => {
  return {
    hello: 'world'
  }
})

> pages/hello.vue
<template>
  <pre>{{ data }}</pre>
</template>

<script setup>
  const { data } = await useFetch('/api/hello');
</script>

Handle file names can be suffixed with .get, .post, .put, .delete, … to match the request’s HTTP Method.

> server/api/test.get.ts
export default defineEventHandler(() => 'Test get handler')

> server/api/test.post.ts
export default defineEventHandler(() => 'Test post handler')

Handle request with Body:

> server/api/submit.post.ts
export default defineEventHandler(async (event) => {
    const body = await readBody(event)
    return { body }
})

Handle request with Query:

> server/api/search.ts
// Template query: /api/search?param1=a&param2=b
export default defineEventHandler((event) => {
  const query = getQuery(event)
  return { a: query.param1, b: query.param2 }
})

 

Conclusion

With the new version of Nuxt, in addition to the growing Vue.js community, the strength of the core technologies has gradually attracted the user community of Vue.js in general and Nuxt in particular. This has helped provide a rich and diverse documentation resource.

Advantage:

  1. Performance has improved.
  2. Improve mobile application performance.
  3. Flexible and modular architectures.
  4. Better Typescript support, plugins, and debugging tools.
  5. SEO optimization.
  6. Integrates well with Vue 3.
  7. Less complicated configuration.

Disadvantage:

  1. For beginners, it is necessary to know Vue.js.
  2. Nuxt requires its standard folder structure. This can be difficult when you need to customize the folder structure or integrate with other structured external tools and libraries.
  3. Some Vue.js libraries and plugins may not be compatible or require additional configuration when used with Nuxt.
  4. Nuxt is a powerful framework designed for complex applications. If you have a simple project or a single-page app, Nuxt can introduce unnecessary complexity and cost.
  5. The community is small and growing.

Statistics between popular rendering frameworks:

(https://npmtrends.com/)

 

(https://2022.stateofjs.com/)

 

Reference:

https://nuxt.com/

https://npmtrends.com/

https://2022.stateofjs.com/