---
title: Development Guide
description: Best practices, patterns, and optimization guidelines for developing with Harmonic SaaS.
navigation:
  icon: i-lucide-code
---

# Development Guide

This guide provides best practices and optimization recommendations for developing applications with Harmonic SaaS.

## Quick Fixes

### Import Missing Error Helper

Add the `createError` import for consistent H3 error handling:

```typescript [server/utils/auth.ts]
import { type H3Event, getHeaders, createError } from 'h3'
```

### Unify Auth Handler Responses

Return a consistent response style from the auth route:

```typescript [server/api/[...auth].ts]
export default defineEventHandler(async (event) => {
  const request = toWebRequest(event)
  try {
    return await auth.handler(request)
  } catch (err) {
    console.error('Auth handler error:', err)
    return new Response(
      JSON.stringify({ error: 'Authentication failed' }),
      { status: 500, headers: { 'content-type': 'application/json' } }
    )
  }
})
```

### Fix i18n Boolean Values

Use booleans instead of strings in locale files:

```json [i18n/locales/en-US.json]
{
  "exact": true
}
```

### Use Localized Paths

Generate paths with `localePath()` instead of hardcoding:

```vue
<script setup lang="ts">
const localePath = useLocalePath()
</script>

<template>
  <NuxtLink :to="localePath('/dashboard/account')" exact>
    Account
  </NuxtLink>
</template>
```

## Nuxt Configuration

### Security and Caching

Add security headers and static asset caching via `routeRules`:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  routeRules: {
    '/**': {
      headers: {
        'x-frame-options': 'DENY',
        'x-content-type-options': 'nosniff',
        'referrer-policy': 'no-referrer',
        // Start with report-only mode, then enforce
        'content-security-policy': "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'"
      }
    },
    '/_nuxt/**': {
      headers: {
        'cache-control': 'public, max-age=31536000, immutable'
      }
    },
    '/public/**': {
      headers: {
        'cache-control': 'public, max-age=31536000, immutable'
      }
    }
  }
})
```

### i18n Performance

Enable lazy loading for better performance:

```typescript [nuxt.config.ts]
i18n: {
  langDir: 'i18n/locales',
  lazy: true,
  locales: [
    { code: 'nl', name: 'Nederlands', iso: 'nl-BE', file: 'nl-BE.json' },
    { code: 'en', name: 'English', iso: 'en-US', file: 'en-US.json' }
  ],
  defaultLocale: 'nl',
  strategy: 'prefix_except_default'
}
```

### Pin Framework Versions

Avoid `"latest"` for critical dependencies:

```json [package.json]
{
  "dependencies": {
    "vue": "^3.5.0",
    "nuxt": "^4.2.0"
  },
  "devDependencies": {
    "@nuxt/eslint": "^0.8.0"
  }
}
```

## Authentication Patterns

### URL Building Safety

Build URLs using `new URL()` to avoid missing slashes:

```typescript [server/utils/auth.ts]
const base = config.public.betterAuthUrl || 'http://localhost:3000'
const acceptUrl = new URL('/api/auth/organization/accept-invitation', base)
acceptUrl.searchParams.set('id', data.invitation.id)
```

### Validate Secrets on Startup

Fail fast in production if required secrets are missing:

```typescript [server/plugins/validate-env.ts]
export default defineNitroPlugin(() => {
  if (process.env.NODE_ENV === 'production') {
    const required = [
      'DATABASE_URL',
      'BETTER_AUTH_SECRET',
      'GITHUB_CLIENT_ID',
      'GITHUB_CLIENT_SECRET'
    ]

    const missing = required.filter(key => !process.env[key])
    if (missing.length > 0) {
      throw new Error(`Missing required environment variables: ${missing.join(', ')}`)
    }
  }
})
```

### Extend requireUserSession

Add optional role/permission checks:

```typescript [server/utils/auth.ts]
export async function requireUserSession(
  event: H3Event,
  options?: { role?: string, permission?: string }
) {
  const session = await getUserSession(event)

  if (!session?.user) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized'
    })
  }

  if (options?.role && session.user.role !== options.role) {
    throw createError({
      statusCode: 403,
      message: 'Forbidden'
    })
  }

  return session
}
```

## Shared Utilities

### Base URL Helper

Create a single source for base URLs:

```typescript [app/utils/url.ts]
export function getBaseUrl() {
  try {
    const config = useRuntimeConfig()
    return config.public.betterAuthUrl || 'http://localhost:3000'
  } catch {
    // Non-Nuxt contexts
    return typeof window !== 'undefined'
      ? window.location.origin
      : 'http://localhost:3000'
  }
}

export function joinUrl(path: string) {
  const base = getBaseUrl()
  return new URL(path, base).toString()
}
```

Use this from both server and client to avoid duplication.

## Email Delivery

### Plaintext Fallback

Auto-generate plaintext versions:

```typescript [server/utils/email.ts]
import { htmlToText } from 'html-to-text'

const textBody = text ?? htmlToText(html)
```

### Structured Logging

Use `consola` instead of `console.log`:

```typescript [server/utils/email.ts]
import { consola } from 'consola'

consola.info('Sending email to:', to)
consola.error('Email failed:', error)
```

### DKIM Configuration

Configure DKIM for SMTP providers and verify domain for Resend to improve deliverability.

## Database Best Practices

### Typed Table Registry

Replace `Record<string, unknown>` with typed dictionaries:

```typescript [server/database/schema.ts]
import type { PgTable } from 'drizzle-orm/pg-core'

interface TableRegistry {
  user: typeof user
  session: typeof session
  // ... other tables
}

export function registerTables<T extends Record<string, PgTable>>(
  tables: T
): void {
  // Implementation
}
```

### SSL for Cloud Providers

Detect cloud providers and enable SSL:

```typescript [server/database/connection.ts]
const isCloudProvider =
  databaseUrl.includes('neon.tech') ||
  databaseUrl.includes('vercel-storage.com')

const config = {
  connection: {
    ssl: isCloudProvider ? { rejectUnauthorized: false } : false
  }
}
```

### Database Health Check

Provide a health check utility:

```typescript [server/utils/database.ts]
import { sql } from 'drizzle-orm'

export async function pingDB() {
  const db = useDB()
  try {
    await db.execute(sql`SELECT 1`)
    return true
  } catch (error) {
    consola.error('Database health check failed:', error)
    return false
  }
}
```

## Internationalization

### Keep Config Out of Translations

Move external URLs to runtime config:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      websiteHostname: 'example.com'
    }
  }
})
```

Keep locale files strictly for UI strings.

### Enable Development Warnings

Catch missing keys early:

```typescript [nuxt.config.ts]
i18n: {
  missingWarn: process.env.NODE_ENV === 'development',
  fallbackWarn: process.env.NODE_ENV === 'development'
}
```

## Middleware and Routing

### Reduce Session Fetches

Only fetch session when needed:

```typescript [app/middleware/auth.global.ts]
export default defineNuxtRouteMiddleware(async (to) => {
  // Only fetch if route requires auth
  if (to.meta.auth !== false) {
    await fetchSession()
  }
})
```

### Locale-Safe Redirects

Use `localePath()` for all redirects:

```typescript
const localePath = useLocalePath()
return navigateTo(localePath('/auth/signin'))
```

## UI Component Patterns

### Passkey Manager UX

Add optimistic updates and loading states:

```vue
<template>
  <div v-if="loading">
    <USkeleton class="h-20" />
  </div>
  <div v-else>
    <!-- Content -->
  </div>
</template>
```

### Format Dates with Locale

```typescript
import { format } from 'date-fns'
import { nl, enUS } from 'date-fns/locale'

const { locale } = useI18n()
const dateLocale = locale.value === 'nl' ? nl : enUS

const formatted = format(new Date(passkey.createdAt), 'PP', {
  locale: dateLocale
})
```

## API Design

### Standardize Server Responses

Choose one approach and use it consistently:

**Option 1: H3 Errors**
```typescript
throw createError({
  statusCode: 400,
  message: 'Invalid input'
})
```

**Option 2: JSON Responses**
```typescript
return {
  error: 'Invalid input',
  code: 'INVALID_INPUT'
}
```

### Input Validation

Validate with Zod and return machine-readable errors:

```typescript
import { z } from 'zod'

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
})

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  const result = schema.safeParse(body)
  if (!result.success) {
    throw createError({
      statusCode: 400,
      message: 'Validation failed',
      data: result.error.issues
    })
  }

  // Process valid data
})
```

### Error Hygiene

Mask internal messages in production:

```typescript
export default defineEventHandler(async (event) => {
  try {
    // Your logic
  } catch (error) {
    consola.error('[API Error]', error) // Log full error server-side

    throw createError({
      statusCode: 500,
      message: process.env.NODE_ENV === 'production'
        ? 'Internal server error'
        : error.message
    })
  }
})
```

## Testing Best Practices

### Unit Test Coverage

Focus on critical utilities:

```typescript
// Test auth utilities
describe('requireUserSession', () => {
  it('throws 401 when no session', async () => {
    const event = createMockEvent()
    await expect(requireUserSession(event)).rejects.toThrow()
  })
})

// Test email utilities
describe('sendEmail', () => {
  it('handles template errors gracefully', async () => {
    // Test implementation
  })
})
```

### E2E Test Patterns

Use `data-testid` selectors for stable tests:

```vue
<template>
  <button data-testid="submit-button">
    Submit
  </button>
</template>
```

```typescript
await page.getByTestId('submit-button').click()
```

### Coverage Thresholds

Enforce minimum coverage:

```typescript [vitest.config.ts]
export default defineConfig({
  test: {
    coverage: {
      lines: 85,
      functions: 85,
      branches: 80,
      statements: 85
    }
  }
})
```

## Security Hardening

### Security Headers Checklist

- **HSTS**: Production only, 1 year max-age
- **X-Frame-Options**: DENY or SAMEORIGIN
- **X-Content-Type-Options**: nosniff
- **Referrer-Policy**: no-referrer or strict-origin-when-cross-origin
- **CSP**: Start in report-only mode

### CSRF Protection

Verify Better Auth CSRF token handling for non-idempotent endpoints.

### Secrets Policy

- Validate required secrets on boot
- Never log secrets
- Use environment variables, never hardcode
- Prefer short-lived tokens for OAuth

## Performance Optimization

### Session Fetch Memoization

Memoize SSR session fetch to prevent duplicate calls:

```typescript
const sessionCache = new Map()

export async function getSession(event: H3Event) {
  const requestId = event.node.req.headers['x-request-id']

  if (sessionCache.has(requestId)) {
    return sessionCache.get(requestId)
  }

  const session = await fetchSession(event)
  sessionCache.set(requestId, session)

  return session
}
```

### Lazy Imports

Dynamically import heavy utilities:

```typescript
// Instead of top-level import
const { render } = await import('@vue-email/render')
```

### Image Optimization

Use `@nuxt/image` for automatic optimization:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    domains: ['example.com'],
    formats: ['webp', 'avif']
  }
})
```

## Priority Implementation Checklist

High-impact changes to implement first:

::steps
1. Add `createError` import in `server/utils/auth.ts`
2. Standardize return types in `server/api/[...auth].ts`
3. Convert i18n `exact` to boolean and use `localePath()`
4. Add `langDir` + `lazy` to i18n config
5. Create shared `getBaseUrl()` and `joinUrl()` helper
6. Add baseline `routeRules` with security headers
7. Pin framework dependency versions
::

## Further Reading

::card-group
  ::card
  ---
  title: Nuxt Security
  icon: i-lucide-shield
  to: https://nuxt.com/docs/guide/going-further/security
  target: _blank
  ---
  Official Nuxt security best practices
  ::

  ::card
  ---
  title: Better Auth Docs
  icon: i-lucide-lock
  to: https://www.better-auth.com/docs
  target: _blank
  ---
  Authentication library documentation
  ::

  ::card
  ---
  title: Drizzle ORM Best Practices
  icon: i-lucide-database
  to: https://orm.drizzle.team/docs/best-practices
  target: _blank
  ---
  Database ORM optimization guide
  ::
::
