16 October 2023

Nuxt Layers Unwrapped

This is a supporting article for my talk Layers Unwrapped at Nuxt Nation 2023.

Resources:

If you have been using Nuxt 2, you'd know that the default Nuxt is pretty minimal. And the same is true for Nuxt 3 as well. But, we can extend Nuxt to do more for us!

Nuxt has runtime context and build-time context. We can use Nuxt plugins to change and extend Nuxt's runtime behaviour. And we can use Nuxt Modules to change and extend Nuxt's build-time behaviour. Here's the brief refresher on both:

useNuxtApp() composable comes packaged with the default Nuxt 3. It helps us access the shared runtime context of Nuxt. It has default methods, hooks and properties. But Nuxt allows us to add custom properties and methods of our own to it! And we can do that using Nuxt plugins.

Nuxt Modules help us tap into different stages of the build-process. As a result, we extend the build-time context of the Nuxt. Look at the signature of Nuxt Module, export default defineNuxtModule((options, nuxt) => { /*...*/ }. The second argument - nuxt - here is nothing but the build-time context. Print nuxt in the terminal to see all default and custom (if any) options added to the Nuxt via nuxt.config file.

Extending NuxtExtending Nuxt

Module vs Layers

Apart from some obvious differences, we have some features overlap between Nuxt Modules and Layers. These similarities may confuse you if you have been using Nuxt Modules for specific use-cases, such as, to add:

  • Nuxt pages
  • Component library
  • Nuxt plugins
  • Server routes
  • State management (Pinia, Vuex), etc

While we can do all the above using Nuxt Modules, we can do the same using Nuxt Layers as well. There's a steep learning curve in authoring Nuxt Modules. Authoring Nuxt Modules requires you to know @nuxt/kit, all the methods it provides, their signatures, how & when to use them, etc. While writing Nuxt Layers feels very much like writing your standard Nuxt application because that's exactly what it is! Here's the other key differences between the two:

Modules:

  • are lower-level construct that extends Nuxt core
  • are mostly associated with integrations and bringing in external libraries inside Nuxt
  • are written inline within nuxt.config file or within /modules directory
  • requires deep knowledge of @nuxt/kit to author Nuxt Modules

Layers:

  • are higher-level construct that help us isolate re-usable parts of Nuxt, such as, Nuxt pages, layouts, components, composable, etc
  • cannot be written inline in nuxt.config like Nuxt Modules
  • do not have any convention-driven directory names unlike the directory structure of Nuxt
  • feel identical to writing standard Nuxt applications

Extending Nuxt with Layers

Until now, we have used Nuxt Modules and Plugins to extend default Nuxt. But with the release of Nuxt 3, now we have Nuxt Layers to extend Nuxt in a whole new way! We can add Nuxt Layers to any Nuxt apps using extends: key of the nuxt.config file of the Nuxt app. This is very similar to how we add Nuxt modules using modules: key.

export default defineNuxtConfig({
  extends: [
    // monorepo or external NPM package as a layer
    '@scope/moduleName',
    // local layers
    '../layers/base'
  ],
  modules: []
})

Nuxt layers can physically be anywhere as long as they have a nuxt.config file in them. Nuxt layers can be:

  1. a local directory,
  2. online code repo, such as Github, Gitlab, BitBucket, etc
  3. an NPM package that's published online
  4. a monorepo package that is accessible locally without having to publish anything online.

Understanding layer-able parts

Follow the indigo-colour layer icon in the diagram below to learn which directories are layer-able. This is an updated diagram of the Nuxt Directory structure. Compared to the one you've seen on my Twitter profile, here we have many more layer-able parts of Nuxt.

Nuxt 3 Layer-able partsNuxt 3 Layer-able parts

Besides /composables, /components, /pages, /server and nuxt.config file, we can also include /layouts, /middelware, /public, /plugins directories along with the app.vue & app.config files into our Nuxt Layers.

Layer-able directories in the diagram may seem like a random collection of folders. But Nuxt layers promote division and encapsulation of distinct parts of the bigger Nuxt application. Studying different use-cases will make a lot more sense later in this article.

I'd invite you to keep track of Github issue 13367 to see any further development in Layer support.

How do layers work?

Nuxt Layers feature uses c12 and defu packages from the unjs ecosystem in the background. nuxt.config file of the Nuxt app points to the nuxt.config file of the layer using extends: key and merges into one. That's why, using multiple different layers in your Nuxt app does not mean we are running multiple Nuxt apps at once.

UnJs - c12UnJs - c12

Going back to the Nuxt Module signature one more time export default defineNuxtModule((options, nuxt) => { /*...*/ }. We can use the second argument - nuxt - to check whether the custom layer directory exists. This is possible when we extend the build-time context of the Nuxt using nuxt.config.

export default defineNuxtModule({
  setup(_options, nuxt) {
    for (const layer of nuxt.options._layers) {
      // You can check for a custom directory existence to extend for each layer
      console.log('Custom extension for', layer.cwd, layer.config)
    }
  }
})

This shows how important it is to have the nuxt.config file in your Nuxt layer directory - even if it is empty! This is a minimum requirement for a directory to be a functioning layer.

A directory must have a nuxt.config file in its root to be considered as a Nuxt layer that can be extended into another Nuxt app.

Examples

Create layer with starter template

Nuxt provides a nuxi command to initialise a starter template with basic structure. In the script below, nuxt-ui-layer is the name of our layer.

npx nuxi init --template layer nuxt-ui-layer

Like Nuxt Modules, you will need a Nuxt app to test your layer as well. This nuxi command will generate a playground project within your layer. This playground acts as your target Nuxt app where you might use and customise your layer. So feel free to try it out yourself. Inspect <HelloWorld /> component, run the playground and try updating the name in /.playground/app.config.ts of your playground to see the layer in action.

// packages/nuxt-ui-layer/.playground/app.config.ts
export default defineAppConfig({
  myLayer: {
    name: 'My amazing Nuxt layer from the playground.'
  }
})

Monorepo packages as layers

We can convert our layer into a monorepo package as well. Let's create a UI layer that provides theming service. Imagine our layer needs few external dependencies to work. For example, Tailwind CSS module for Nuxt in this instance. So, using Nuxt layer in a form of NPM package takes care of installing these dependencies for us.

We can keep using the playground to test our layer. But let's create a new Nuxt app in our /apps directory right next to the /packages. This way we can install our monorepo package, i.e. UI Layer. And add it as a layer in nuxt.config of the Nuxt app.

Step by step: convert layer into monorepo package

Click here to review Finished project on Github
  • Create /packages and /apps directories in the project root.
  • Move nuxt-ui-layer into the /packages directory. Later, we will create a fresh Nuxt app in /apps that will use Nuxt Layers from the /packages.
  • In nuxt-ui-layer > package.json > name the package, so that we can use it later in our Nuxt app.
// packages/nuxt-ui-layer/package.json
{
  "name": "@kru/nuxt-ui-layer"
}
  • Create pnpm-workspace.yaml in the project root and specify possible workspaces, so that we can share packages within this project.
// pnpm-workspace.yaml
packages:
  - "packages/**"
  - "apps/**"
  • Run pnpm init to create package.json
// package.json
{
  // ...
  "packageManager": "pnpm@8.6.2”
}

Creating a monorepo package offloads the responsibility of installing Tailwind CSS over to the UI layer. This way our Nuxt app in /apps directory does not need to worry about installing any dependencies.

Step by step: Add UI Layer in new Nuxt project

  • Create new Nuxt app in /apps directory
npx nuxi@latest init nuxt-app-1
There are two steps to add nuxt-ui-layer into this brand new Nuxt app.
  1. First, add our UI layer as an NPM package in package.json and run pnpm i to install @kru/nuxt-ui-layer
// apps/nuxt-app-1/package.json
"@kru/nuxt-ui-layer": "workspace:*"
  1. And second, extend the layer in nuxt.config.ts and test the <HelloWorld /> component - this time, in nuxt-app-1 and not in the playground!
// apps/nuxt-app-1/nuxt.config.ts
export default defineNuxtConfig({
  extends: [
    '@kru/nuxt-ui-layer'
  ]
})

Theming with layer

nuxt.config keeps configurations related to Nuxt customisation. In a similar way, app.config keeps configurations related to your application customisation. We can use this app.config to create a customisable theme with default options.

We rarely use the colour names as they come with CSS frameworks like Tailwind when we work on client projects. These colours rather become primary colour palette, secondary colour palette and so on. This way we can update the palettes with ease when there is a change in branding.

In this example, we want to define default primary and secondary colours in the app.config of our UI Layer. And then configure Tailwind CSS to use these custom colour palettes. Then we should be able to override these default colour palettes when using this layer.

Step by step: from app.config to Nuxt Plugin to Tailwind CSS

  • Add default colour palettes in app.config of the UI Layer. You can use uicolors.app to create your custom colour palettes.
// packages/nuxt-ui-layer/app.config.ts
export default defineAppConfig({
  myLayer: {
    name: 'Hello from Nuxt layer',
    theme: {
      primary: {
        '50': '#fcffe5',
        '100': '#f6ffc7',
        // ...
        '950': '#213201',
      },
      secondary: { /*...*/ }
    }
  }
})
  • Create a Nuxt plugin to convey the new colours over to the browser.
// packages/nuxt-ui-layer/plugins/theme.ts
export default defineNuxtPlugin((nuxtApp) => {
  // convert individual palette to css variables that CSS can understand
  const style = individualPalette(useAppConfig()?.myLayer?.theme)
  if (style) {
     useHead({
        style: [
          {
             children: `:root, body { ${style} }`
          }
       ]
    })
  }
})
After implementing the above plugin, we should see var(--primary-50) to var(--primary-950) in the Styles inspection panel of the browser.Next, we tell Tailwind to use these colours through tailwind.config.
// packages/nuxt-ui-layer/tailwind.config.ts
const primary = {
    '50': 'var(--primary-50)',
    '100': 'var(--primary-100)',
    // ...
    '950': 'var(--primary-950)',
}
const secondary = { /* ... */ }

module.exports = {
  theme: {
    extend: {
      colors: {
        primary,
        secondary
      }
    }
  }
}

We have added our own colour palettes using app.config and Nuxt plugin. Try adding background with bg-primary-100 in <HelloWorld /> component to see theming in action. You can even go beyond the colour palette to provide a customisable UI layer.

We have already configured this UI layer at /apps/nuxt-app-1. Let's try to override the default primary colours to something else. This is where the real power of Nuxt Layers comes to light!

Step by step: override primary colour palette

Add new colours to app.config of the Nuxt app that's using our UI layer.Make sure the shape of your app.config matches exactly as the app.config of your Nuxt layer that's providing the theme.
// apps/nuxt-app-1/app.config.ts
export default defineAppConfig({
    myLayer: {
        name: "Hello from my first Nuxt App!",
        theme: {
            primary: {
                '50': '#fef1f7',
                '100': '#fee5f0',
                '200': '#fecce3',
                '300': '#ffa2cb',
                '400': '#fe68a7',
                '500': '#f83c86',
                '600': '#e91f64',
                '700': '#ca0c47',
                '800': '#a70d3b',
                '900': '#8b1034',
                '950': '#55021a'
            }
        }
    }
})

You should see an updated primary colour background in your <HelloWorld /> component.

In the next example, we will make use of the primary colour to theme one of the UI components and then override the primary colour from our nuxt-app-1.

Share component library using layer

Nothing should stop you from separating your component-library layer from the theme layer. But to show the concept, let's add an Input component next to our <HelloWorld /> component right in our UI layer. Now, this is no ordinary Input component! It uses primary colour and icons. It validates and aims to include a set of unit tests as well. It uses the following libraries to achieve all this:

  • @iconify/vue,
  • @vuelidate/core,
  • @vuelidate/validators,
  • @vue/test-utils and
  • vitest.

Click here to see all the files that make up this custom Input component.

We will install all these dependencies in our UI layer, like we did for the Tailwind CSS module earlier. So that, the Nuxt app using this layer won't need to worry about installing them.

Now, you should be able to use this Input component in the Nuxt app at /apps/nuxt-app-1!

<!-- apps/nuxt-app-1/app.vue -->
<template>
  <div class="p-10">
    <Input v-model="amount"
          label="Amount"
          icon="mdi:dollar"
          helpText="Enter dollar value without decimals."
          :validation="validationRules.amount">
      <template #trailing>
        AUD
      </template>
    </Input> 
  </div>
</template>

Play with default and custom colour palettes to see how easy it is to theme Vue components with Nuxt Layers!

Provide GraphQL API from layer

We have created a project structure with /packages directory for NPM (layer) packages. And /apps directory for potential Nuxt apps.

Let's create a new layer as an NPM package to configure GraphQL API and share it with other Nuxt apps. When your Nuxt layer provides data services, it's fair to call it a nuxt-data-layer.

npx nuxi init --template layer nuxt-data-layer

Let's name this layer in package.json.

// packages/nuxt-data-layer/package.json
{
  "name": "@kru/nuxt-data-layer",
}

We use GraphQL Yoga - graphql-yoga - to set up and configure the GraphQL server. Then we use the @nuxtjs/apollo module to consume GraphQL API endpoints on our Nuxt pages. We install both of these dependencies in nuxt-data-layer itself.

In this layer, we use Nitro server engine features using /server directory. Click here to see the finished layer that provides GraphQL API. Run the playground app from the nuxt-data-layer with the code below. And we should see the GraphQL interface up and running at localhost:3000/api/graphql .

// packages/nuxt-data-layer/server/api/graphql.ts
import { createYoga } from "graphql-yoga"
import schema from '../../graphql'
export default defineEventHandler(async (event) => {
    const yoga = createYoga({ schema, graphqlEndpoint: '/api/graphql' })
    const { req, res } = event.node
    return yoga(req, res)
})

Then we install @kru/nuxt-data-layer through package.json and add it in nuxt.config of our Nuxt app - nuxt-app-1. We can use useAsycQuery from @nuxt/apollo to consume the data as below.

// apps/nuxt-app-1/app.vue
<script setup lang="ts">
// GRAPHQL QUERY
const query = gql`
  query greeting {
    hello
  }
`
const { data } = await useAsyncQuery(query)
</script>

Feature specific parts of Nuxt with local layers

When I say local layers, I mean to say that these layers do not have any dependencies to install. And these local layers do not have to be within your Nuxt app either. This is one of my favourite use-cases of Nuxt layers.

I have three local layers:

  1. base - This layer has a dynamic page that resolves dynamic Vue components based on the URL params. And that's all it does. You could have more complex business logic to resolve these components dynamically. But the given example is enough for the PoC.
  2. pdp - stands for product detail page.
  3. plp - stands for product listing page.

Both pdp and plp layers are unaware of where and how the base layer renders them. When we extend these three layers together in our Nuxt app, they work together seamlessly. See all three local layers in the Github repo of this project.

Thinking in Layers

Layers can have a significant impact on how we structure our Nuxt app. But how to think about layers when we build large applications using Nuxt 3. Imagine if these layers were a part of an actual Nuxt project. We can then divide each layers into one of these categories:

Nuxt LayersNuxt Layers

> Platform layers: To abstract UI library, theming, authentication, data provider or even reusable Nuxt layouts.

> Feature specific layers: Let's take an e-commerce app as an example. We can have one layer for each features, such as:

  • PDP (Product detail page),
  • PLP (Product listing page),
  • Checkout,
  • Account, etc.

> App layers: Layers are not limited to a feature or a functionality. They can have an entire app abstracted into them. For example, blogs, landing pages, marketing campaign apps. They can be re-used for creating multi-variant websites out of a foundation layer.

We have barely scratched a surface with these examples. We can take each one of these concepts a lot further into more advanced use-cases. These categories are not set in stone just yet because we are still in the early stages of Nuxt Layers. We will see more defined types of layers and the way of using them as this feature matures.

Hope my talk/article on Nuxt Layer has inspired you to explore Nuxt 3 and Nuxt Layers. Follow me at @krutiepatel to get notified of my upcoming articles and diagrams on Nuxt 3.