Skip to content

Development Guide

Frontend Development

Technology Stack

  • Vue 3 Composition API - Write components using modern Composition API
  • TypeScript - Strict mode ensuring type safety
  • Element Plus - UI component library based on Material Design
  • Tailwind CSS - Atomic CSS framework
  • Nuxt 4 - Full-stack framework providing SSR and API capabilities

Component Development

Creating Components

vue
<!-- app/components/MyComponent.vue -->
<script setup lang="ts">
// Using TypeScript types
interface Props {
  title: string
  count?: number
}

// Define Props
const props = withDefaults(defineProps<Props>(), {
  count: 0
})

// Define Emits
const emit = defineEmits<{
  update: [value: number]
  delete: []
}>()

// Reactive data
const message = ref('Hello World')
const items = ref<string[]>([])

// Computed properties
const doubledCount = computed(() => props.count * 2)

// Methods
const handleClick = () => {
  emit('update', props.count + 1)
}

// Lifecycle
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <div class="my-component">
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
    <el-button @click="handleClick">
      Count: {{ count }}
    </el-button>
  </div>
</template>

<style scoped lang="css">
.my-component {
  /* Styles */
}
</style>

Using Composable Functions

typescript
// app/composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const doubledCount = computed(() => count.value * 2)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count,
    doubledCount,
    increment,
    decrement,
    reset
  }
}
vue
<!-- Using in components -->
<script setup lang="ts">
const { count, increment, decrement, reset } = useCounter(0)
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>

API Request Wrapper

typescript
// app/composables/useApi.ts
export const useApi = () => {
  const config = useRuntimeConfig()

  const request = async <T>(url: string, options?: RequestInit): Promise<T> => {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          'Content-Type': 'application/json',
          ...options?.headers
        }
      })

      const data = await response.json()

      if (!response.ok || !data.success) {
        throw new Error(data.message || 'Request failed')
      }

      return data.data
    } catch (error) {
      console.error('API request error:', error)
      throw error
    }
  }

  return { request }
}

Page Development

vue
<!-- app/pages/articles/[slug].vue -->
<script setup lang="ts">
// Get route parameters
const route = useRoute()
const slug = route.params.slug

// Get article data
const { data: article, pending, error } = await useFetch(`/api/articles/${slug}`)

// SEO optimization
useSeoMeta({
  title: article.value?.title || 'Article Details',
  description: article.value?.summary,
  ogTitle: article.value?.title,
  ogImage: article.value?.cover_image
})
</script>

<template>
  <div>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Failed to load</div>
    <article v-else-if="article">
      <h1>{{ article.title }}</h1>
      <p>{{ article.summary }}</p>
      <div v-html="article.content"></div>
    </article>
  </div>
</template>

Backend Development

API Route Development

Nuxt 3 uses file system routing, API files are placed in server/api/ directory.

typescript
// server/api/hello.get.ts
export default defineEventHandler(async (event) => {
  return {
    success: true,
    message: 'Hello World'
  }
})

API with Parameters

typescript
// server/api/articles/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  
  // Get article
  const article = await ArticleModel.findById(id)
  
  if (!article) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Article not found'
    })
  }
  
  return {
    success: true,
    data: article
  }
})

POST Request

typescript
// server/api/articles/create.post.ts
export default defineEventHandler(async (event) => {
  try {
    // Read request body
    const body = await readBody(event)
    
    // Get current user (requires authentication middleware)
    const user = event.context.user
    
    // Parameter validation
    if (!body.title || !body.content) {
      return {
        success: false,
        message: 'Title and content cannot be empty'
      }
    }
    
    // Create article
    const article = await ArticleModel.create({
      ...body,
      author_id: user.userId
    })
    
    return {
      success: true,
      data: article,
      message: 'Created successfully'
    }
  } catch (error) {
    console.error('Failed to create article:', error)
    return {
      success: false,
      message: 'Creation failed'
    }
  }
})

Middleware Development

typescript
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // Skip login related paths
  if (['/api/auth/login'].includes(event.path)) {
    return
  }

  // Get Token
  const cookies = parseCookies(event)
  const token = cookies.auth_token

  if (token) {
    const payload = verifyToken(token)
    if (payload) {
      // Verify user status
      const user = await User.getSafeUserById(payload.userId)
      if (user && user.status == 'active') {
        event.context.user = user
        return
      }
    }
  }

  // Authentication failed
  if (event.path.startsWith('/api/admin/')) {
    throw createError({
      statusCode: 401,
      message: 'No permission to access admin API'
    })
  }
})

Data Model Development

typescript
// server/models/Article.ts
import { db } from 'server/database/config'

export const ArticleModel = {
  // Find all articles
  async findAll(limit: number = 20, offset: number = 0) {
    const [rows] = await db.query(`
      SELECT a.*, c.name as category_name
      FROM articles a
      LEFT JOIN categories c ON a.category_id = c.id
      ORDER BY a.created_at DESC
      LIMIT ? OFFSET ?
    `, [limit, offset])
    return rows
  },

  // Find article by ID
  async findById(id: number) {
    const [rows] = await db.query(`
      SELECT a.*, c.name as category_name
      FROM articles a
      LEFT JOIN categories c ON a.category_id = c.id
      WHERE a.id = ?
    `, [id])
    return rows[0]
  },

  // Create article
  async create(data: any) {
    const [result] = await db.query(`
      INSERT INTO articles (title, content, summary, author_id, category_id)
      VALUES (?, ?, ?, ?, ?)
    `, [data.title, data.content, data.summary, data.author_id, data.category_id])
    
    return result.insertId
  },

  // Update article
  async update(id: number, data: any) {
    const [result] = await db.query(`
      UPDATE articles 
      SET title = ?, content = ?, summary = ?, category_id = ?, updated_at = NOW()
      WHERE id = ?
    `, [data.title, data.content, data.summary, data.category_id, id])
    
    return result.affectedRows > 0
  },

  // Delete article
  async delete(id: number) {
    const [result] = await db.query('DELETE FROM articles WHERE id = ?', [id])
    return result.affectedRows > 0
  }
}

Database Operations

Using MySQL Plugin

typescript
import { db } from 'server/database/config'

export default defineEventHandler(async (event) => {
  // Query
  const [users] = await db.query('SELECT * FROM users WHERE status = ?', ['active'])
  
  // Insert
  const [result] = await db.query(
    'INSERT INTO users (username, email, password) VALUES (?, ?, ?)',
    ['test', 'test@example.com', 'hashed_password']
  )
  
  // Update
  await db.query('UPDATE users SET nickname = ? WHERE id = ?', ['New Name', userId])
  
  // Delete
  await db.query('DELETE FROM users WHERE id = ?', [userId])
  
  return { success: true }
})

Using Transactions

typescript
import { db } from 'server/database/config'

export default defineEventHandler(async (event) => {
  const connection = await db.getConnection()
  
  try {
    await connection.beginTransaction()
    
    // Execute multiple operations
    await connection.query('INSERT INTO articles (...) VALUES (...)', [...])
    await connection.query('UPDATE categories SET count = count + 1 WHERE id = ?', [categoryId])
    
    await connection.commit()
    return { success: true }
  } catch (error) {
    await connection.rollback()
    throw error
  } finally {
    connection.release()
  }
})

Code Standards

TypeScript Standards

typescript
// ✅ Good practices
interface User {
  id: number
  username: string
  email: string
}

const getUser = async (id: number): Promise<User> => {
  // ...
}

// ❌ Avoid
const getUser = async (id) => {
  // Missing type annotations
}

Naming Conventions

TypeConventionExample
ComponentPascalCaseUserProfile.vue
Utility functioncamelCaseformatDate()
ConstantUPPER_SNAKE_CASEAPI_BASE_URL
Interface/TypePascalCaseUserProfile
Filenamekebab-caseuser-profile.ts

Comment Standards

typescript
/**
 * User authentication service
 * Provides user login, registration, Token validation and other functions
 */
export class AuthService {
  /**
   * User login
   * @param username Username
   * @param password Password
   * @returns Login result
   */
  static async login(username: string, password: string): Promise<LoginResult> {
    // ...
  }
}

Debugging Techniques

Development Tools

bash
# Type checking
pnpm run type-check

# Code linting
pnpm run lint

# Auto fix
pnpm run lint:fix

Log Debugging

typescript
export default defineEventHandler(async (event) => {
  console.log('Request path:', event.path)
  console.log('Query parameters:', getQuery(event))
  console.log('Request body:', await readBody(event))
  
  // ...
})

Testing

TODO: Add testing framework and examples

Next Steps