This is a supporting article for my talk Layers Unwrapped
at Nuxt Nation 2023.
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.
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:
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:
nuxt.config
file or within /modules
directory@nuxt/kit
to author Nuxt ModulesLayers:
nuxt.config
like Nuxt ModulesUntil 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:
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.
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.
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.
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.
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.'
}
})
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
/packages
and /apps
directories in the project root.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
.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"
}
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/**"
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
/apps
directorynpx nuxi@latest init nuxt-app-1
There are two steps to add nuxt-ui-layer
into this brand new Nuxt app.package.json
and run pnpm i
to install @kru/nuxt-ui-layer
// apps/nuxt-app-1/package.json
"@kru/nuxt-ui-layer": "workspace:*"
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'
]
})
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
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: { /*...*/ }
}
}
})
// 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
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
.
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
andvitest
.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!
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>
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:
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.pdp
- stands for product detail page
.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.
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:
> 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:
> 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.