iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Avancé

OpenCaddyHost

Construire un dashboard Nuxt 4 pour piloter Caddy via son Admin API — authentification JWT, reverse proxy CRUD, import/export, et déploiement Docker Compose.

Nuxt 4Vue 3TypeScriptPiniaCaddyDocker
GitHub
01Setup et configuration02L'API Admin de Caddy03Server routes Nuxt 404Authentification JWT05Client Caddy et gestion des proxies06Interface et composants Vue07Déploiement Docker Compose
Chapitre 5·30 min

Client Caddy et gestion des proxies

Le cœur d'OpenCaddyHost : le client qui parle à l'Admin API Caddy, et les server routes qui exposent les opérations CRUD au dashboard.

Le client Caddy

server/utils/caddy.ts encapsule tous les appels à l'Admin API. Les server routes l'importent sans se soucier de la mécanique HTTP.

server/utils/caddy.ts
interface CaddyProxy {
  id: string
  name: string
  domain: string
  target: string
  ssl: boolean
  headers?: Record<string, string>
  createdAt?: string
}
 
export async function getCaddyConfig(): Promise<any> {
  const config = useRuntimeConfig()
  const response = await fetch(`${config.caddyAdminUrl}/config/`)
  if (!response.ok) throw new Error(`Caddy API error: ${response.status}`)
  return response.json()
}
 
export async function getCaddyProxies(): Promise<CaddyProxy[]> {
  const caddyConfig = await getCaddyConfig()
  if (!caddyConfig) return []
 
  const routes = caddyConfig?.apps?.http?.servers?.srv0?.routes ?? []
 
  return routes.map((route: any) => {
    const id = route['@id'] ?? ''
    const domain = route.match?.[0]?.host?.[0] ?? ''
    const handler = route.handle?.find((h: any) => h.handler === 'reverse_proxy')
    const target = handler?.upstreams?.[0]?.dial ?? ''
 
    return {
      id,
      name: id.replace('proxy-', ''),
      domain,
      target,
      ssl: route.match?.[0]?.host !== undefined,
      createdAt: route.createdAt,
    }
  })
}
 
export async function createCaddyProxy(proxy: Omit<CaddyProxy, 'id'>): Promise<void> {
  const config = useRuntimeConfig()
  const id = `proxy-${proxy.name.toLowerCase().replace(/\s+/g, '-')}`
 
  const route = buildRoute(id, proxy)
 
  // Lire la config actuelle
  const current = await getCaddyConfig() ?? {
    apps: { http: { servers: { srv0: { listen: [':80'], routes: [] } } } }
  }
 
  // Ajouter la route
  current.apps.http.servers.srv0.routes.push(route)
 
  // Envoyer la config mise à jour
  const response = await fetch(`${config.caddyAdminUrl}/load`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(current),
  })
 
  if (!response.ok) throw new Error(`Failed to create proxy: ${response.status}`)
}
 
export async function deleteCaddyProxy(id: string): Promise<void> {
  const config = useRuntimeConfig()
  const response = await fetch(`${config.caddyAdminUrl}/id/${id}`, {
    method: 'DELETE',
  })
  if (!response.ok) throw new Error(`Failed to delete proxy: ${response.status}`)
}

Construire une route Caddy

server/utils/caddy.ts
function buildRoute(id: string, proxy: Omit<CaddyProxy, 'id'>): object {
  const handler: any = {
    handler: 'reverse_proxy',
    upstreams: [{ dial: proxy.target }],
  }
 
  // Headers personnalisés
  if (proxy.headers && Object.keys(proxy.headers).length > 0) {
    handler.headers = {
      request: {
        set: Object.entries(proxy.headers).reduce((acc, [key, value]) => {
          acc[key] = [value]
          return acc
        }, {} as Record<string, string[]>),
      },
    }
  }
 
  return {
    '@id': id,
    match: [{ host: [proxy.domain] }],
    handle: [handler],
    terminal: true,
    createdAt: new Date().toISOString(),
  }
}

terminal: true dit à Caddy d'arrêter d'évaluer les routes suivantes si celle-ci correspond — comportement attendu pour un reverse proxy.

Server routes CRUD

server/api/proxies.get.ts
export default defineEventHandler(async (event) => {
  authenticateToken(event)
 
  const { search, status } = getQuery(event)
  let proxies = await getCaddyProxies()
 
  if (search) {
    const q = (search as string).toLowerCase()
    proxies = proxies.filter(p =>
      p.name.toLowerCase().includes(q) ||
      p.domain.toLowerCase().includes(q) ||
      p.target.toLowerCase().includes(q)
    )
  }
 
  return {
    success: true,
    data: proxies,
    message: `${proxies.length} proxy${proxies.length > 1 ? 's' : ''} trouvé${proxies.length > 1 ? 's' : ''}`,
  }
})
server/api/proxies.post.ts
export default defineEventHandler(async (event) => {
  authenticateToken(event)
 
  const body = await readBody(event)
  const { name, domain, target, ssl, headers } = body
 
  if (!name || !domain || !target) {
    throw createError({ statusCode: 400, statusMessage: 'name, domain et target sont requis' })
  }
 
  await createCaddyProxy({ name, domain, target, ssl: ssl ?? false, headers })
 
  return { success: true, message: `Proxy "${name}" créé` }
})

Endpoint de santé Caddy

server/api/caddy/health.get.ts
export default defineEventHandler(async (event) => {
  authenticateToken(event)
  const config = useRuntimeConfig()
 
  try {
    const response = await fetch(`${config.caddyAdminUrl}/config/`, {
      signal: AbortSignal.timeout(3000), // timeout 3s
    })
    return {
      healthy: response.ok,
      statusCode: response.status,
      url: config.caddyAdminUrl,
    }
  } catch (error: any) {
    return {
      healthy: false,
      error: error.message,
      url: config.caddyAdminUrl,
    }
  }
})

AbortSignal.timeout(3000) annule la requête si Caddy ne répond pas en 3 secondes — évite de bloquer le dashboard indéfiniment.

Appeler les routes depuis le dashboard

app/composables/useProxies.ts
export function useProxies() {
  const { getAuthHeaders } = useAuth()
 
  const proxies = ref<Proxy[]>([])
  const loading = ref(false)
 
  async function fetchProxies(search?: string) {
    loading.value = true
    try {
      const data = await $fetch('/api/proxies', {
        headers: getAuthHeaders(),
        query: search ? { search } : {},
      })
      proxies.value = data.data
    } finally {
      loading.value = false
    }
  }
 
  async function createProxy(payload: CreateProxyPayload) {
    await $fetch('/api/proxies', {
      method: 'POST',
      headers: getAuthHeaders(),
      body: payload,
    })
    await fetchProxies() // rafraîchir la liste
  }
 
  async function deleteProxy(id: string) {
    await $fetch(`/api/proxies/${id}`, {
      method: 'DELETE',
      headers: getAuthHeaders(),
    })
    await fetchProxies()
  }
 
  return { proxies, loading, fetchProxies, createProxy, deleteProxy }
}

$fetch est le client HTTP natif de Nuxt — il gère la sérialisation JSON et lance des erreurs typées sur les status 4xx/5xx.

Précédent
Authentification JWT
Suivant
Interface et composants Vue

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main