Skip to content

Secondary Development Best Practices

A comprehensive guide for efficiently extending and customizing the Vue admin template. This guide focuses on practical patterns and strategies that help you build upon the template foundation while maintaining code quality and development velocity.

Organize your code by features rather than file types for better maintainability:

src/
β”œβ”€β”€ components/ # Shared components
β”‚ β”œβ”€β”€ ui/ # Base UI components (buttons, inputs, etc.)
β”‚ β”œβ”€β”€ business/ # Business logic components
β”‚ └── layout/ # Layout-specific components
β”œβ”€β”€ features/ # Feature modules
β”‚ β”œβ”€β”€ user-management/
β”‚ β”‚ β”œβ”€β”€ components/ # Feature-specific components
β”‚ β”‚ β”‚ β”œβ”€β”€ UserTable.vue
β”‚ β”‚ β”‚ β”œβ”€β”€ UserForm.vue
β”‚ β”‚ β”‚ └── UserFilters.vue
β”‚ β”‚ β”œβ”€β”€ composables/ # Feature-specific composables
β”‚ β”‚ β”‚ β”œβ”€β”€ useUserData.ts
β”‚ β”‚ β”‚ └── useUserValidation.ts
β”‚ β”‚ β”œβ”€β”€ services/ # API services
β”‚ β”‚ β”‚ └── userService.ts
β”‚ β”‚ β”œβ”€β”€ types/ # TypeScript types
β”‚ β”‚ β”‚ └── user.types.ts
β”‚ β”‚ └── views/ # Feature pages
β”‚ β”‚ β”œβ”€β”€ UserListView.vue
β”‚ β”‚ └── UserDetailView.vue
β”‚ β”œβ”€β”€ product-management/
β”‚ └── dashboard/
β”œβ”€β”€ shared/ # Shared utilities and services
β”‚ β”œβ”€β”€ services/ # Global services
β”‚ β”œβ”€β”€ composables/ # Shared composables
β”‚ β”œβ”€β”€ utils/ # Utility functions
β”‚ └── constants/ # Application constants
└── stores/ # Pinia stores
β”œβ”€β”€ auth.ts
β”œβ”€β”€ app.ts
└── modules/ # Feature-specific stores
β”œβ”€β”€ user.ts
└── product.ts

Benefits:

  • Easier Navigation: Related files are grouped together
  • Better Encapsulation: Feature-specific code is self-contained
  • Improved Scalability: New features can be added without affecting existing structure
  • Team Collaboration: Multiple developers can work on different features independently

Organize configuration based on environments and features:

Type-Safe Configuration Management
// config/index.ts
import { z } from 'zod'
// Configuration schema for validation
const ConfigSchema = z.object({
app: z.object({
name: z.string(),
version: z.string(),
environment: z.enum(['development', 'staging', 'production']),
}),
api: z.object({
baseUrl: z.string().url(),
timeout: z.number().default(10000),
retries: z.number().default(3),
}),
features: z.object({
enableNotifications: z.boolean().default(true),
enableAnalytics: z.boolean().default(false),
maxFileUploadSize: z.number().default(10485760), // 10MB
}),
ui: z.object({
theme: z.enum(['light', 'dark', 'auto']).default('auto'),
language: z.string().default('en'),
pageSize: z.number().default(20),
})
})
// Type-safe configuration
type AppConfig = z.infer<typeof ConfigSchema>
// Environment-specific configurations
const baseConfig: AppConfig = {
app: {
name: 'Vue Admin Template',
version: '1.0.0',
environment: (import.meta.env.MODE as any) || 'development',
},
api: {
baseUrl: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
timeout: 10000,
retries: 3,
},
features: {
enableNotifications: true,
enableAnalytics: import.meta.env.PROD,
maxFileUploadSize: 10485760,
},
ui: {
theme: 'auto',
language: 'en',
pageSize: 20,
}
}
// Development overrides
const developmentConfig: Partial<AppConfig> = {
api: {
...baseConfig.api,
timeout: 30000, // Longer timeout for development
},
features: {
...baseConfig.features,
enableAnalytics: false,
}
}
// Production overrides
const productionConfig: Partial<AppConfig> = {
api: {
...baseConfig.api,
baseUrl: import.meta.env.VITE_API_BASE_URL || 'https://api.yourdomain.com',
},
features: {
...baseConfig.features,
enableAnalytics: true,
}
}
// Merge configurations
const envConfigs = {
development: developmentConfig,
staging: baseConfig,
production: productionConfig,
}
const mergedConfig = {
...baseConfig,
...envConfigs[baseConfig.app.environment],
}
// Validate and export
export const config = ConfigSchema.parse(mergedConfig)
// Type-safe config access
export type { AppConfig }

Create reusable data providers for common operations:

Reusable Data Provider
// composables/useDataProvider.ts
export function useDataProvider<T>(
fetchFn: () => Promise<T[]>,
options: {
immediate?: boolean
refetchOnWindowFocus?: boolean
staleTime?: number
cacheKey?: string
} = {}
) {
const {
immediate = true,
refetchOnWindowFocus = false,
staleTime = 5 * 60 * 1000, // 5 minutes
cacheKey
} = options
const data = ref<T[]>([])
const loading = ref(false)
const error = ref<Error | null>(null)
const lastFetch = ref<number>(0)
// Cache management
const cache = new Map<string, { data: T[], timestamp: number }>()
const fetch = async (force = false) => {
// Check cache first
if (cacheKey && !force) {
const cached = cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < staleTime) {
data.value = cached.data
return
}
}
try {
loading.value = true
error.value = null
const result = await fetchFn()
data.value = result
lastFetch.value = Date.now()
// Update cache
if (cacheKey) {
cache.set(cacheKey, { data: result, timestamp: Date.now() })
}
} catch (err) {
error.value = err instanceof Error ? err : new Error('Unknown error')
console.error('Data fetch error:', err)
} finally {
loading.value = false
}
}
const refetch = () => fetch(true)
// Auto-fetch on mount
if (immediate) {
onMounted(() => fetch())
}
// Refetch on window focus
if (refetchOnWindowFocus) {
useEventListener('visibilitychange', () => {
if (!document.hidden && Date.now() - lastFetch.value > staleTime) {
fetch()
}
})
}
return {
data: readonly(data),
loading: readonly(loading),
error: readonly(error),
fetch,
refetch
}
}
// Usage in components:
const { data: users, loading, error, refetch } = useDataProvider(
() => userService.getUsers(),
{
cacheKey: 'users',
refetchOnWindowFocus: true
}
)

Extend the theme system using CSS custom properties for consistent styling:

Extended Theme System
/* styles/theme.css */
:root {
/* Brand Colors */
--brand-primary: #3b82f6;
--brand-secondary: #6366f1;
--brand-accent: #f59e0b;
/* Extended Color Palette */
--color-success-50: #f0fdf4;
--color-success-500: #22c55e;
--color-success-900: #14532d;
--color-warning-50: #fffbeb;
--color-warning-500: #f59e0b;
--color-warning-900: #78350f;
--color-error-50: #fef2f2;
--color-error-500: #ef4444;
--color-error-900: #7f1d1d;
/* Component-specific variables */
--table-header-bg: var(--color-gray-50);
--table-row-hover: var(--color-gray-25);
--form-field-border: var(--color-gray-300);
--form-field-focus: var(--brand-primary);
/* Layout variables */
--sidebar-width: 280px;
--header-height: 64px;
--content-max-width: 1400px;
/* Animation variables */
--animation-fast: 150ms;
--animation-normal: 300ms;
--animation-slow: 500ms;
/* Shadow system */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
/* Dark theme overrides */
[data-theme="dark"] {
--table-header-bg: var(--color-gray-800);
--table-row-hover: var(--color-gray-700);
--form-field-border: var(--color-gray-600);
}
/* Component styling using custom properties */
.custom-table {
--table-border-color: var(--form-field-border);
}
.custom-table th {
background-color: var(--table-header-bg);
border-bottom: 1px solid var(--table-border-color);
}
.custom-table tr:hover {
background-color: var(--table-row-hover);
}
/* Utility classes for custom styling */
.bg-brand-primary {
background-color: var(--brand-primary);
}
.text-brand-primary {
color: var(--brand-primary);
}
.border-brand-primary {
border-color: var(--brand-primary);
}