Jiří ProcházkaArticles

Jiří ProcházkaArticles

VueNuxt#frontend

Nuxt 3 Layers: The Building Blocks of a SaaS Factory - UI Layer

4 weeks ago - 29 min read #frontend, #nuxt, #tailwindcss, #layers

In the world of software development, modularity and reusability are the keys to scaling efficiently. Nuxt 3 introduces an exciting feature called layers—a powerful way to create modular, reusable applications. Think of layers as the assembly lines in your SaaS factory, where each line is responsible for a specific component of your app, from the UI to business logic.

For example, in my SaaS factory project, I've designed the architecture to include multiple layers: a UI layer, a database layer, and a Stripe layer. This approach lets me reuse these components across projects or even across teams, without duplicating code.

In this blog series, I’ll show you how to use Nuxt 3 layers effectively. This first part will focus on setting up the UI layer with TailwindCSS and referencing it in the main app.

Why Use Layers in Nuxt 3?

Layers allow you to:

  • Encapsulate Functionality: Separate UI components, business logic, and utilities into distinct modules.
  • Promote Reusability: Share a common UI or utility layer across multiple projects.
  • Streamline Collaboration: Teams can focus on specific layers without stepping on each other’s toes.
  • Simplify Maintenance: Bug fixes or updates in one layer automatically propagate to all projects using it.

For a SaaS factory, this means you can build once and deploy often.

The Goal: A TailwindCSS-Powered UI Layer

In this article, we’ll create a reusable UI layer for styling your app. It will be powered by TailwindCSS and set up in a way that makes it easy to integrate into your main Nuxt 3 application—or any other Nuxt 3 project.

Step 1: Create PNPM Workspaces

Create a file pnpm-worspace.yaml in the project root and insert:

// pnpm-workspace.yaml
packages:
  - "packages/**"
  - "apps/**"

run:

$ pnpm init

Step 2: Create the UI Layer

$ mkdir packages
$ cd packages
$ pnpx nuxi init --template layer ui

Let me explain what these commands do:

  • mkdir packages - Creates a new directory named "packages"
  • cd packages - Changes the current working directory to the newly created "packages" directory
  • pnpx nuxi init --template layer ui - Initializes a new Nuxt Layer project using a UI template. This creates a base UI layer package that can be shared across multiple Nuxt applications, containing reusable UI components, composables, and utilities.

Step 3: Set the UI layer name

Setting the UI layer name so we can reference it as a NPM package in the main app:

// packages/ui/package.json
{
  "name": "@jiprochazka/ui"
}

Step 4: Installing Tailwindcss

$ cd packages/ui
$ pnpx nuxi@latest module add tailwindcss
// packages/ui/nuxt.config.ts

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  experimental: { appManifest: false },
  modules: ["@nuxtjs/tailwindcss"]
})

Step 5: Creating the app

Go to the project root and run:

$ mkdir apps
$ cd apps
$ pnpx nuxi@latest init my-app

Here's what these commands do:

  • mkdir apps - Creates a new directory named "apps"
  • cd apps - Changes the current working directory to the newly created "apps" directory
  • pnpx nuxi@latest init my-app - Initializes a new Nuxt application using the latest version of Nuxt. The app will be created in a directory named "my-app". This command creates a fresh Nuxt project with all the necessary files and dependencies to start building your web application.

Step 6: Referencing the UI layer

In the app's package.json file we must se the UI layer as a dependency:

// apps/my-app/package.json
{
  "name": "@jiprochazka/my-app",  // set the project name
  ...
   "dependencies": {
     "@jiprochazka/ui": "workspace:*",  // here must be the exact name from the 'packages/ui/package.json'
    ...
  }
}

In the nuxt.config.ts file the app must extend the UI layer:

// apps/my-app/nuxt.config.ts

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: true },
  extends: ["@jiprochazka/ui"]
})

Step 7: Setting PNPM command in the root and installation

For a convenience we can set few pnpm commands in the root package.json:

{
  "scripts": {
    ...
    "packages": "pnpm --filter",
    "ui": "pnpm packages @jiprochazka/ui",
    "my-app": "pnpm packages @jiprochazka/my-app"
  }
}

Here's what these scripts in the root package.json do:

  • "packages": "pnpm --filter" - A base command that allows running commands for specific workspaces in your monorepo using PNPM's filter feature
  • "ui": "pnpm packages @jiprochazka/ui" - A shorthand to run commands specifically for your UI layer package. You can use it like pnpm ui dev or pnpm ui build
  • "my-app": "pnpm packages @jiprochazka/my-app" - Similar shorthand for your Nuxt application. Usage: pnpm my-app dev or pnpm my-app build

These scripts make it easier to run commands for specific parts of your monorepo without having to change directories or use longer filter commands.

Tailwindcss settings

When you define a tailwind.config.js in your UI layer, it serves as a base configuration with your design system's core styles, theme, and components. However, applications using this layer can extend or override these settings by creating their own tailwind.config.js. Nuxt 3 automatically merges these configurations, with the main app's settings taking precedence. This allows you to maintain consistent base styling through the UI layer while giving each application the flexibility to customize its appearance by extending or overwriting specific Tailwind classes, colors, spacing, or other design tokens.
Example:

// packages/ul/tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: [],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        primary: {
          50: "#eff6ff",
          100: "#dbeafe",
          200: "#bfdbfe",
          300: "#93c5fd",
          400: "#60a5fa",
          500: "#3b82f6",
          600: "#2563eb",
          700: "#1d4ed8",
          800: "#1e40af",
          900: "#1e3a8a",
          950: "#172554"
        }
      }
    }
  },
  plugins: []
}
// apps/my-app/tailwind-config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [],
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        primary: {
          50: "#fef2f2",
          100: "#fee2e2",
          200: "#fecaca",
          300: "#fca5a5",
          400: "#f87171",
          500: "#ef4444",
          600: "#dc2626",
          700: "#b91c1c",
          800: "#991b1b",
          900: "#7f1d1d",
          950: "#450a0a"
        }
      }
    }
  },
  plugins: []
}

Now the my-app colors configuration will overwrite the default ones.

Next Up in the Series

In this article, we set the foundation by creating a reusable UI layer. In the next part, we’ll explore adding a Drizzle into the database layer. Stay tuned for more ways to supercharge your SaaS factory with Nuxt 3 layers!

Jiří Procházka Newsletter

Want to get new tips for Vue & Nuxt?