Join the Newsletter!

Exclusive content & updates. No spam.

Skip to content

Atomic Architecture: Revolutionizing Vue and Nuxt Project Structure

Published:Β atΒ 

Introduction

Clear writing requires clear thinking. The same is valid for coding. Throwing all components into one folder may work when starting a personal project. But as projects grow, especially with larger teams, this approach leads to problems:

Atomic Design offers a solution. Let’s examine how to apply it to a Nuxt project.

What is Atomic Design

atomic design diagram brad Frost

Brad Frost developed Atomic Design as a methodology for creating design systems. It is structured into five levels inspired by chemistry:

  1. Atoms: Basic building blocks (e.g. form labels, inputs, buttons)
  2. Molecules: Simple groups of UI elements (e.g. search forms)
  3. Organisms: Complex components made of molecules/atoms (e.g. headers)
  4. Templates: Page-level layouts
  5. Pages: Specific instances of templates with content

For Nuxt, we can adapt these definitions:

Code Example: Before and After

Consider this non-Atomic Design todo app component:

Screenshot of ToDo App

<template>
  <div class="container mx-auto p-4">
    <h1 class="text-2xl font-bold mb-4 text-gray-800 dark:text-gray-200">Todo App</h1>
    
    <!-- Add Todo Form -->
    <form @submit.prevent="addTodo" class="mb-4">
      <input
        v-model="newTodo"
        type="text"
        placeholder="Enter a new todo"
        class="border p-2 mr-2 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded"
      />
      <button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded transition duration-300">
        Add Todo
      </button>
    </form>
    
    <!-- Todo List -->
    <ul class="space-y-2">
      <li
        v-for="todo in todos"
        :key="todo.id"
        class="flex justify-between items-center p-3 bg-gray-100 dark:bg-gray-700 rounded shadow-sm"
      >
        <span class="text-gray-800 dark:text-gray-200">{{ todo.text }}</span>
        <button
          @click="deleteTodo(todo.id)"
          class="bg-red-500 hover:bg-red-600 text-white p-1 rounded transition duration-300"
        >
          Delete
        </button>
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

interface Todo {
  id: number
  text: string
}

const newTodo = ref('')
const todos = ref<Todo[]>([])

const fetchTodos = async () => {
  // Simulating API call
  todos.value = [
    { id: 1, text: 'Learn Vue.js' },
    { id: 2, text: 'Build a Todo App' },
    { id: 3, text: 'Study Atomic Design' }
  ]
}

const addTodo = async () => {
  if (newTodo.value.trim()) {
    // Simulating API call
    const newTodoItem: Todo = {
      id: Date.now(),
      text: newTodo.value
    }
    todos.value.push(newTodoItem)
    newTodo.value = ''
  }
}

const deleteTodo = async (id: number) => {
  // Simulating API call
  todos.value = todos.value.filter(todo => todo.id !== id)
}

onMounted(fetchTodos)
</script>

This approach leads to large, difficult-to-maintain components. Let’s refactor using Atomic Design:

This will be the refactored structure

πŸ“ Template (Layout)
   β”‚
   └─── πŸ“„ Page (TodoApp)
        β”‚
        └─── πŸ“¦ Organism (TodoList)
             β”‚
             β”œβ”€β”€β”€ πŸ§ͺ Molecule (TodoForm)
             β”‚    β”‚
             β”‚    β”œβ”€β”€β”€ βš›οΈ Atom (BaseInput)
             β”‚    └─── βš›οΈ Atom (BaseButton)
             β”‚
             └─── πŸ§ͺ Molecule (TodoItems)
                  β”‚
                  └─── πŸ§ͺ Molecule (TodoItem) [multiple instances]
                       β”‚
                       β”œβ”€β”€β”€ βš›οΈ Atom (BaseText)
                       └─── βš›οΈ Atom (BaseButton)

Refactored Components

Tempalte Default

<template>
  <div class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-300">
    <header class="bg-white dark:bg-gray-800 shadow">
      <nav class="container mx-auto px-4 py-4 flex justify-between items-center">
        <NuxtLink to="/" class="text-xl font-bold">Todo App</NuxtLink>
        <ThemeToggle />
      </nav>
    </header>
    <main class="container mx-auto px-4 py-8">
      <slot />
    </main>
  </div>
</template>

<script setup lang="ts">
import ThemeToggle from '~/components/ThemeToggle.vue'
</script>

Pages


<script setup lang="ts">
import { ref, onMounted } from 'vue'
import TodoList from '../components/organisms/TodoList'

interface Todo {
  id: number
  text: string
}

const todos = ref<Todo[]>([])

const fetchTodos = async () => {
  // Simulating API call
  todos.value = [
    { id: 1, text: 'Learn Vue.js' },
    { id: 2, text: 'Build a Todo App' },
    { id: 3, text: 'Study Atomic Design' }
  ]
}

const addTodo = async (text: string) => {
  // Simulating API call
  const newTodoItem: Todo = {
    id: Date.now(),
    text
  }
  todos.value.push(newTodoItem)
}

const deleteTodo = async (id: number) => {
  // Simulating API call
  todos.value = todos.value.filter(todo => todo.id !== id)
}

onMounted(fetchTodos)
</script>

<template>
  <div class="container mx-auto p-4">
    <h1 class="text-2xl font-bold mb-4 text-gray-800 dark:text-gray-200">Todo App</h1>
    <TodoList 
      :todos="todos"
      @add-todo="addTodo"
      @delete-todo="deleteTodo"
    />
  </div>
</template>

Organism (TodoList)


<script setup lang="ts">
import TodoForm from '../molecules/TodoForm.vue'
import TodoItem from '../molecules/TodoItem.vue'

interface Todo {
  id: number
  text: string
}

defineProps<{
  todos: Todo[]
}>()

defineEmits<{
  (e: 'add-todo', value: string): void
  (e: 'delete-todo', id: number): void
}>()
</script>

<template>
  <div>
    <TodoForm @add-todo="$emit('add-todo', $event)" />
    <ul class="space-y-2">
      <TodoItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
        @delete-todo="$emit('delete-todo', $event)"
      />
    </ul>
  </div>
</template>

Molecules (TodoForm and TodoItem)

TodoForm.vue:
<script setup lang="ts">
import TodoForm from '../molecules/TodoForm.vue'
import TodoItem from '../molecules/TodoItem.vue'

interface Todo {
  id: number
  text: string
}

defineProps<{
  todos: Todo[]
}>()

defineEmits<{
  (e: 'add-todo', value: string): void
  (e: 'delete-todo', id: number): void
}>()
</script>

<template>
  <div>
    <TodoForm @add-todo="$emit('add-todo', $event)" />
    <ul class="space-y-2">
      <TodoItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
        @delete-todo="$emit('delete-todo', $event)"
      />
    </ul>
  </div>
</template>

TodoItem.vue:

<script setup lang="ts">
import { ref } from 'vue'
import BaseInput from '../atoms/BaseInput.vue'
import BaseButton from '../atoms/BaseButton.vue'

const newTodo = ref('')
const emit = defineEmits<{
  (e: 'add-todo', value: string): void
}>()

const addTodo = () => {
  if (newTodo.value.trim()) {
    emit('add-todo', newTodo.value)
    newTodo.value = ''
  }
}
</script>

<template>
  <form @submit.prevent="addTodo" class="mb-4">
    <BaseInput v-model="newTodo" placeholder="Enter a new todo" />
    <BaseButton type="submit">Add Todo</BaseButton>
  </form>
</template>

Atoms (BaseButton, BaseInput, BaseText)

BaseButton.vue:
<script setup lang="ts">
defineProps<{
  variant?: 'primary' | 'danger'
}>()
</script>

<template>
  <button
    :class="[
      'p-2 rounded transition duration-300',
      variant === 'danger'
        ? 'bg-red-500 hover:bg-red-600 text-white'
        : 'bg-blue-500 hover:bg-blue-600 text-white'
    ]"
  >
    <slot></slot>
  </button>
</template>

BaseInput.vue:


<script setup lang="ts">
defineProps<{
  modelValue: string
  placeholder?: string
}>()
defineEmits<{
  (e: 'update:modelValue', value: string): void
}>()
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
    type="text"
    :placeholder="placeholder"
    class="border p-2 mr-2 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded"
  />
</template>
Component LevelJobExamples
AtomsPure, single-purpose componentsBaseButton BaseInput BaseIcon BaseText
MoleculesCombinations of atoms with minimal logicSearchBar LoginForm StarRating Tooltip
OrganismsLarger, self-contained, reusable components. Can perform side effects and complex operations.TheHeader ProductCard CommentSection NavigationMenu
TemplatesNuxt layouts defining page structureDefaultLayout BlogLayout DashboardLayout AuthLayout
PagesComponents handling data and API callsHomePage UserProfile ProductList CheckoutPage

Summary

Atomic Design offers one path to a more apparent code structure. It works well as a starting point for many projects. But as complexity grows, other architectures may serve you better. Want to explore more options? Read my post on How to structure vue Projects. It covers approaches beyond Atomic Design when your project outgrows its initial structure.

Stay Updated!

Subscribe to my newsletter for more TypeScript, Vue, and web dev insights directly in your inbox.

  • Background information about the articles
  • Weekly Summary of all the interesting blog posts that I read
  • Small tips and trick
Subscribe Now

Most Related Posts