Skip to content

Code Style and Patterns Guide

Last Updated: 2025-11-22 Target Audience: Contributors, Developers

Coding standards and patterns used in the VueSIP project.


Table of Contents

  1. Overview
  2. TypeScript Conventions
  3. Vue 3 Patterns
  4. Naming Conventions
  5. Code Organization
  6. Error Handling
  7. Documentation
  8. Code Quality Tools

Overview

VueSIP follows strict coding standards to ensure consistency, maintainability, and quality across the codebase.

Core Principles:

  • TypeScript First: All code must be fully typed
  • Composition API: Use Vue 3 Composition API patterns
  • Functional Style: Prefer pure functions and immutability
  • Explicit Over Implicit: Be clear and explicit in your code
  • DRY: Don't repeat yourself, but avoid premature abstraction

TypeScript Conventions

Type Definitions

typescript
// ✅ Good: Explicit types for function parameters and return values
function makeCall(target: string, options?: CallOptions): Promise<CallSession> {
  // Implementation
}

// ❌ Bad: No type annotations
function makeCall(target, options) {
  // Implementation
}

Interface vs Type

typescript
// ✅ Good: Use interfaces for object shapes
export interface CallSession {
  id: string
  state: CallState
  direction: CallDirection
}

// ✅ Good: Use type for unions, intersections, primitives
export type CallState = 'idle' | 'connecting' | 'active' | 'ended'
export type ExtendedSession = CallSession & { metadata: Record<string, unknown> }

// ❌ Bad: Using type for simple object shapes
export type CallSession = {
  id: string
  state: CallState
}

Avoid any

typescript
// ❌ Bad: Using any
function process(data: any) {
  return data.value
}

// ✅ Good: Use proper types
function process(data: Record<string, unknown>) {
  return data.value
}

// ✅ Better: Use generics
function process<T extends { value: unknown }>(data: T) {
  return data.value
}

Null vs Undefined

typescript
// ✅ Good: Use undefined for optional values
interface CallOptions {
  timeout?: number  // undefined when not provided
}

// ✅ Good: Use null for intentional absence
let activeCall: CallSession | null = null  // No call active

Type Guards

typescript
// ✅ Good: Implement type guards for runtime type checking
function isCallSession(value: unknown): value is CallSession {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'state' in value
  )
}

// Usage
if (isCallSession(data)) {
  console.log(data.id) // TypeScript knows data is CallSession
}

Vue 3 Patterns

Composition API

typescript
// ✅ Good: Use Composition API with script setup
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useSipClient } from '@/composables/useSipClient'

const count = ref(0)
const doubled = computed(() => count.value * 2)

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

// ❌ Bad: Don't use Options API
<script lang="ts">
export default {
  data() {
    return { count: 0 }
  },
  computed: {
    doubled() {
      return this.count * 2
    }
  }
}
</script>

Composable Patterns

typescript
// ✅ Good: Composable naming and structure
export function useSipClient(config: SipClientConfig) {
  // State
  const isConnected = ref(false)
  const error = ref<Error | null>(null)

  // Computed
  const canMakeCall = computed(() => isConnected.value && !error.value)

  // Methods
  async function connect(): Promise<void> {
    try {
      // Connection logic
      isConnected.value = true
    } catch (e) {
      error.value = e as Error
      throw e
    }
  }

  // Cleanup
  onUnmounted(() => {
    // Cleanup logic
  })

  // Return public API
  return {
    // State
    isConnected: readonly(isConnected),
    error: readonly(error),

    // Computed
    canMakeCall,

    // Methods
    connect,
  }
}

Reactive References

typescript
// ✅ Good: Use ref for primitives and reactive for objects
const count = ref(0)
const user = reactive({ name: 'John', age: 30 })

// ✅ Good: Use readonly for exposing state
return {
  count: readonly(count),
  user: readonly(user),
}

// ❌ Bad: Exposing mutable state directly
return {
  count,  // Consumers can modify this
  user,   // Consumers can modify this
}

Naming Conventions

Files and Directories

✅ Good file naming:
- useSipClient.ts        (composables)
- CallSession.ts         (classes)
- sip.types.ts           (types)
- logger.ts              (utilities)
- configStore.ts         (stores)

❌ Bad file naming:
- sip-client.ts
- call_session.ts
- SipTypes.ts

Variables and Functions

typescript
// ✅ Good: Descriptive names with proper casing
const isConnected = ref(false)
const selectedAudioDevice = ref<MediaDevice | null>(null)

function makeOutboundCall(target: string): Promise<CallSession> {
  // Implementation
}

// ❌ Bad: Unclear or poorly cased names
const conn = ref(false)
const dev = ref(null)

function call(t: string) {
  // Implementation
}

Constants

typescript
// ✅ Good: UPPER_SNAKE_CASE for true constants
export const MAX_CALL_DURATION = 3600
export const DEFAULT_TIMEOUT = 30000
export const SIP_STATUS_OK = 200

// ✅ Good: Configuration objects
export const DEFAULT_CONFIG = {
  timeout: 30000,
  retryAttempts: 3,
} as const

Boolean Variables

typescript
// ✅ Good: Use is/has/can/should prefixes
const isConnected = ref(false)
const hasActiveCall = computed(() => calls.value.length > 0)
const canMakeCall = computed(() => isRegistered.value)
const shouldRetry = (attempts: number) => attempts < MAX_RETRIES

// ❌ Bad: Ambiguous boolean names
const connected = ref(false)
const active = computed(() => calls.value.length > 0)

Code Organization

File Structure

typescript
/**
 * File header with module description
 */

// 1. Imports - grouped and ordered
import { ref, computed, onUnmounted } from 'vue'  // Framework imports
import type { CallSession } from '@/types'         // Type imports
import { logger } from '@/utils/logger'           // Utility imports

// 2. Constants
const MAX_RETRIES = 3
const TIMEOUT_MS = 5000

// 3. Types and interfaces (if not in separate file)
export interface UseSipClientOptions {
  autoConnect?: boolean
  timeout?: number
}

// 4. Main implementation
export function useSipClient(options: UseSipClientOptions) {
  // Implementation
}

// 5. Helper functions
function validateConfig(config: unknown): boolean {
  // Implementation
}

Import Order

typescript
// 1. Vue imports
import { ref, computed, watch } from 'vue'

// 2. External libraries
import JsSIP from 'jssip'

// 3. Types (with 'type' keyword)
import type { CallSession, SipConfig } from '@/types'

// 4. Internal modules (grouped by type)
import { SipClient } from '@/core/SipClient'
import { useSipClient } from '@/composables/useSipClient'
import { logger } from '@/utils/logger'
import { configStore } from '@/stores/configStore'

Directory Structure

src/
├── composables/     # Vue composables
├── core/            # Core business logic classes
├── stores/          # Pinia stores
├── types/           # TypeScript type definitions
├── utils/           # Utility functions
├── plugins/         # Vue plugins
└── index.ts         # Public API exports

Error Handling

Error Types

typescript
// ✅ Good: Define custom error classes
export class SipConnectionError extends Error {
  constructor(message: string, public code?: number) {
    super(message)
    this.name = 'SipConnectionError'
  }
}

export class CallFailedError extends Error {
  constructor(message: string, public reason: string) {
    super(message)
    this.name = 'CallFailedError'
  }
}

Try-Catch Patterns

typescript
// ✅ Good: Specific error handling with logging
async function connect(): Promise<void> {
  try {
    await client.connect()
    logger.info('Connected successfully')
  } catch (error) {
    const err = error instanceof Error ? error : new Error('Connection failed')
    logger.error('Connection failed', err)
    lastError.value = err
    throw err  // Re-throw after handling
  }
}

// ❌ Bad: Silent error swallowing
async function connect() {
  try {
    await client.connect()
  } catch (error) {
    // Error ignored
  }
}

Error State Management

typescript
// ✅ Good: Track errors in composables
export function useSipClient() {
  const lastError = ref<Error | null>(null)

  async function connect() {
    lastError.value = null  // Clear previous error
    try {
      await client.connect()
    } catch (error) {
      lastError.value = error as Error
      throw error
    }
  }

  return {
    lastError: readonly(lastError),
    connect,
  }
}

Documentation

JSDoc Comments

typescript
/**
 * Makes an outbound SIP call to the specified target
 *
 * @param target - The SIP URI or phone number to call
 * @param options - Optional call configuration
 * @returns Promise that resolves to the active call session
 * @throws {CallFailedError} If the call setup fails
 *
 * @example
 * ```typescript
 * const session = await makeCall('sip:user@example.com')
 * console.log('Call established:', session.id)
 * ```
 */
export async function makeCall(
  target: string,
  options?: CallOptions
): Promise<CallSession> {
  // Implementation
}

Inline Comments

typescript
// ✅ Good: Explain why, not what
// Retry connection because network might be temporarily unavailable
if (retryAttempts < MAX_RETRIES) {
  await connect()
}

// ❌ Bad: Stating the obvious
// Increment retry attempts
retryAttempts++

Module Documentation

typescript
/**
 * SIP Call Session Management
 *
 * Provides composables for managing SIP call sessions including:
 * - Making outbound calls
 * - Receiving inbound calls
 * - Call state management
 * - DTMF tone sending
 *
 * @module composables/useCallSession
 */

Code Quality Tools

ESLint

bash
# Run linter
pnpm lint

# Fix auto-fixable issues
pnpm lint:fix

Key Rules:

  • No unused variables
  • Consistent quotes (single quotes)
  • No console.log (use logger)
  • Explicit return types for functions

Prettier

bash
# Format code
pnpm format

Configuration:

  • 2 spaces for indentation
  • Single quotes
  • Trailing commas
  • Semicolons required

TypeScript Compiler

bash
# Type check
pnpm typecheck

Settings:

  • Strict mode enabled
  • No implicit any
  • Strict null checks
  • No unused locals/parameters

Commit Hooks

Pre-commit hooks run automatically:

  1. Lint staged files: ESLint on changed files
  2. Format: Prettier on changed files
  3. Type check: TypeScript compilation

Best Practices Summary

DO:

  • ✅ Write fully typed TypeScript
  • ✅ Use Composition API with <script setup>
  • ✅ Export readonly state from composables
  • ✅ Use descriptive variable and function names
  • ✅ Handle errors explicitly with logging
  • ✅ Write JSDoc comments for public APIs
  • ✅ Keep functions small and focused
  • ✅ Use const for values that don't change

DON'T:

  • ❌ Use any type
  • ❌ Use Options API
  • ❌ Expose mutable state directly
  • ❌ Use single-letter variable names
  • ❌ Swallow errors silently
  • ❌ Write functions longer than 50 lines
  • ❌ Use var keyword
  • ❌ Ignore linter warnings

Additional Resources


Questions or Suggestions?

Open a GitHub Discussion to discuss code style improvements.

Released under the MIT License.