Skip to content

Frontend Project Structure

Overview

This section explains the frontend project structure using Vue.js with modern architecture patterns and best practices.

Technology Stack

Core Framework

  • Vue.js 3 - Progressive JavaScript framework
  • TypeScript - Type-safe JavaScript
  • Vite - Fast build tool and dev server

State Management

  • Pinia - Intuitive state management for Vue
  • VueUse - Collection of Vue composition utilities

UI Framework

  • Tailwind CSS - Utility-first CSS framework
  • Headless UI - Unstyled, accessible UI components

Project Structure

Main Application Structure

frontend/
├── src/                        # Main source code
│   ├── components/             # Reusable Vue components
│   │   ├── ui/                # Base UI components
│   │   │   ├── Button/
│   │   │   │   ├── Button.vue
│   │   │   │   ├── Button.test.ts
│   │   │   │   └── index.ts
│   │   │   ├── Input/
│   │   │   ├── Modal/
│   │   │   └── index.ts
│   │   ├── forms/             # Form components
│   │   ├── layout/            # Layout components
│   │   └── business/          # Business-specific components
│   ├── views/                 # Page components
│   │   ├── Home/
│   │   │   ├── Home.vue
│   │   │   ├── Home.test.ts
│   │   │   └── index.ts
│   │   ├── Dashboard/
│   │   ├── Profile/
│   │   └── Auth/
│   ├── router/                # Vue Router configuration
│   │   ├── index.ts           # Main router
│   │   ├── routes/            # Route definitions
│   │   │   ├── auth.ts
│   │   │   ├── dashboard.ts
│   │   │   └── index.ts
│   │   └── guards/            # Route guards
│   ├── stores/                # Pinia stores
│   │   ├── auth.ts            # Authentication store
│   │   ├── user.ts            # User store
│   │   ├── app.ts             # App state store
│   │   └── index.ts
│   ├── services/              # API services
│   │   ├── api/               # API client
│   │   │   ├── client.ts      # HTTP client setup
│   │   │   ├── auth.ts        # Auth API
│   │   │   ├── users.ts       # Users API
│   │   │   └── index.ts
│   │   ├── storage/           # Local storage utilities
│   │   └── validation/        # Form validation
│   ├── utils/                 # Utility functions
│   │   ├── constants.ts       # Application constants
│   │   ├── helpers.ts         # Helper functions
│   │   ├── formatters.ts      # Data formatters
│   │   └── validators.ts      # Validation utilities
│   ├── types/                 # TypeScript type definitions
│   │   ├── api.ts             # API response types
│   │   ├── components.ts      # Component prop types
│   │   └── global.ts          # Global types
│   ├── composables/           # Vue composables
│   │   ├── useAuth.ts         # Authentication composable
│   │   ├── useApi.ts          # API composable
│   │   ├── useForm.ts         # Form handling composable
│   │   └── useToast.ts        # Toast notifications
│   ├── assets/                # Static assets
│   │   ├── images/            # Image files
│   │   ├── icons/             # Icon files
│   │   ├── styles/            # Global styles
│   │   │   ├── main.css       # Main stylesheet
│   │   │   ├── variables.css  # CSS variables
│   │   │   └── utilities.css  # Utility classes
│   │   └── fonts/             # Font files
│   ├── plugins/               # Vue plugins
│   │   ├── i18n.ts            # Internationalization
│   │   ├── toast.ts           # Toast notifications
│   │   └── index.ts
│   ├── layouts/               # Layout components
│   │   ├── DefaultLayout.vue
│   │   ├── AuthLayout.vue
│   │   └── DashboardLayout.vue
│   ├── middleware/            # Route middleware
│   │   ├── auth.ts            # Authentication middleware
│   │   ├── guest.ts           # Guest-only middleware
│   │   └── index.ts
│   ├── config/                # Configuration files
│   │   ├── app.ts             # App configuration
│   │   ├── api.ts             # API configuration
│   │   └── constants.ts       # App constants
│   └── main.ts                # Application entry point
├── public/                    # Public static files
│   ├── favicon.ico
│   ├── robots.txt
│   └── manifest.json
├── tests/                     # Test files
│   ├── unit/                  # Unit tests
│   ├── integration/           # Integration tests
│   ├── e2e/                   # End-to-end tests
│   ├── fixtures/              # Test data
│   └── setup/                 # Test setup files
├── docs/                      # Component documentation
├── .env.example               # Environment variables example
├── .env.local                 # Local environment variables
├── vite.config.ts             # Vite configuration
├── tsconfig.json              # TypeScript configuration
├── tailwind.config.js         # Tailwind CSS configuration
├── package.json               # Dependencies and scripts
└── README.md                  # Project documentation

Component Architecture

Component Structure

Each component should follow this structure:

ComponentName/
├── ComponentName.vue          # Main component file
├── ComponentName.test.ts      # Unit tests
├── ComponentName.stories.ts   # Storybook stories (optional)
├── types.ts                   # Component-specific types
└── index.ts                   # Component export

Component Naming Conventions

  • PascalCase for component names: UserProfile.vue
  • kebab-case for file names: user-profile.vue
  • camelCase for props and events: userName, onUserUpdate

Component Categories

1. UI Components (Base)

vue
<!-- src/components/ui/Button/Button.vue -->
<template>
  <button
    :class="buttonClasses"
    :disabled="disabled"
    @click="handleClick"
  >
    <slot />
  </button>
</template>

<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
  disabled: false
})

const emit = defineEmits<{
  click: [event: MouseEvent]
}>()

const buttonClasses = computed(() => [
  'btn',
  `btn-${props.variant}`,
  `btn-${props.size}`,
  { 'btn-disabled': props.disabled }
])

const handleClick = (event: MouseEvent) => {
  if (!props.disabled) {
    emit('click', event)
  }
}
</script>

2. Business Components

vue
<!-- src/components/business/UserCard/UserCard.vue -->
<template>
  <div class="user-card">
    <img :src="user.avatar" :alt="user.name" class="user-avatar" />
    <div class="user-info">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
    <Button @click="handleEdit">Edit</Button>
  </div>
</template>

<script setup lang="ts">
import { Button } from '@/components/ui'

interface User {
  id: string
  name: string
  email: string
  avatar: string
}

interface Props {
  user: User
}

defineProps<Props>()

const emit = defineEmits<{
  edit: [user: User]
}>()

const handleEdit = () => {
  emit('edit', props.user)
}
</script>

State Management with Pinia

Store Structure

typescript
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
  const isLoading = ref(false)

  // Getters
  const isAuthenticated = computed(() => !!token.value)
  const userRole = computed(() => user.value?.role || 'guest')

  // Actions
  const login = async (credentials: LoginCredentials) => {
    isLoading.value = true
    try {
      const response = await authApi.login(credentials)
      user.value = response.user
      token.value = response.token
      localStorage.setItem('token', response.token)
    } catch (error) {
      throw error
    } finally {
      isLoading.value = false
    }
  }

  const logout = () => {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  return {
    // State
    user,
    token,
    isLoading,
    // Getters
    isAuthenticated,
    userRole,
    // Actions
    login,
    logout
  }
})

Routing Configuration

Router Setup

typescript
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { authGuard, guestGuard } from '@/middleware'
import routes from './routes'

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Global guards
router.beforeEach(authGuard)
router.beforeEach(guestGuard)

export default router

Route Definitions

typescript
// src/router/routes/auth.ts
import { RouteRecordRaw } from 'vue-router'

export const authRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Auth/Login.vue'),
    meta: {
      layout: 'auth',
      requiresGuest: true
    }
  },
  {
    path: '/register',
    name: 'Register',
    component: () => import('@/views/Auth/Register.vue'),
    meta: {
      layout: 'auth',
      requiresGuest: true
    }
  }
]

API Integration

API Client Setup

typescript
// src/services/api/client.ts
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// Request interceptor
apiClient.interceptors.request.use(
  (config) => {
    const authStore = useAuthStore()
    if (authStore.token) {
      config.headers.Authorization = `Bearer ${authStore.token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// Response interceptor
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      const authStore = useAuthStore()
      authStore.logout()
      router.push('/login')
    }
    return Promise.reject(error)
  }
)

export default apiClient

Service Layer

typescript
// src/services/api/users.ts
import apiClient from './client'
import type { User, CreateUserRequest, UpdateUserRequest } from '@/types'

export const usersApi = {
  getAll: () => apiClient.get<User[]>('/users'),
  
  getById: (id: string) => apiClient.get<User>(`/users/${id}`),
  
  create: (user: CreateUserRequest) => 
    apiClient.post<User>('/users', user),
  
  update: (id: string, user: UpdateUserRequest) => 
    apiClient.put<User>(`/users/${id}`, user),
  
  delete: (id: string) => apiClient.delete(`/users/${id}`)
}

Composables

Custom Composables

typescript
// src/composables/useApi.ts
import { ref } from 'vue'

export function useApi<T>(apiCall: () => Promise<T>) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  const execute = async () => {
    loading.value = true
    error.value = null
    
    try {
      data.value = await apiCall()
    } catch (err) {
      error.value = err as Error
    } finally {
      loading.value = false
    }
  }

  return {
    data,
    error,
    loading,
    execute
  }
}

Testing Structure

Test Organization

tests/
├── unit/                      # Unit tests
│   ├── components/            # Component tests
│   ├── stores/                # Store tests
│   ├── utils/                 # Utility tests
│   └── composables/           # Composable tests
├── integration/               # Integration tests
├── e2e/                       # End-to-end tests
├── fixtures/                  # Test data
└── setup/                     # Test setup

Component Testing Example

typescript
// tests/unit/components/ui/Button.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Button from '@/components/ui/Button/Button.vue'

describe('Button', () => {
  it('renders slot content', () => {
    const wrapper = mount(Button, {
      slots: {
        default: 'Click me'
      }
    })
    
    expect(wrapper.text()).toBe('Click me')
  })

  it('emits click event when clicked', async () => {
    const wrapper = mount(Button)
    
    await wrapper.trigger('click')
    
    expect(wrapper.emitted('click')).toBeTruthy()
  })

  it('applies correct classes based on props', () => {
    const wrapper = mount(Button, {
      props: {
        variant: 'danger',
        size: 'lg'
      }
    })
    
    expect(wrapper.classes()).toContain('btn-danger')
    expect(wrapper.classes()).toContain('btn-lg')
  })
})

Build Configuration

Vite Configuration

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true
      }
    }
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['@headlessui/vue', '@heroicons/vue']
        }
      }
    }
  }
})

Best Practices

Code Organization

  1. Component Composition: Use composition API for better logic reuse
  2. Type Safety: Leverage TypeScript for better development experience
  3. Performance: Use lazy loading for routes and components
  4. Accessibility: Follow WCAG guidelines for inclusive design
  5. Responsive Design: Mobile-first approach with Tailwind CSS

Development Workflow

  1. Component-Driven Development: Build components in isolation
  2. Testing: Write tests for all components and business logic
  3. Code Quality: Use ESLint and Prettier for consistent code style
  4. Git Workflow: Follow conventional commits and feature branches
  5. Documentation: Document complex components and business logic