Skip to main content

chdev-frontend

Frontend architecture conventions for the chdev Vue 3 + Vuetify 3 project. Activate when working on frontend code, creating new views, components, composables, services, or routes.

Source facts

Repository
bioleyl/chdev
Last source activity
June 23, 2026 at 14:44
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
chdev-frontend
description
Frontend architecture conventions for the chdev Vue 3 + Vuetify 3 project. Activate when working on frontend code, creating new views, components, composables, services, or routes.
# chdev Frontend Architecture Vue 3 (Composition API + `<script setup>`) + Vuetify 3 + TypeScript + Vite. This skill covers the **frontend** package exclusively; backend is handled by the `chdev-backend` skill. ## Build **Whenever a file in the `common/` folder is changed**, rebuild the common package from the project root: ```bash npm run build -w common ``` This must be done before the frontend can pick up the updated schemas and types. ## Tech Stack - **Runtime**: Node.js, ESM - **Framework**: Vue 3 (Composition API, `<script setup>`, `<script generic>`) - **UI**: Vuetify 3 (Vue 3 component library) - **Validation**: Zod (schemas live in `common/src/schemas/`) - **Routing**: Vue Router 4 - **Build**: Vite - **Linting**: Biome ## Project Layout ``` frontend/src/ main.ts # App bootstrap: Vuetify + Router + permission directive App.vue # Root layout: v-app, v-app-bar, v-main, v-footer env.d.ts # Vite env type declarations router/ index.ts # Vue Router setup with auth guard (beforeEach) services/ api.service.ts # Generic HTTP client: request, get, post, put, delete, getBlob, paginate auth.service.ts # Auth: login, logout, token management, role checks (singleton class) <resource>.service.ts # Resource API service (class with static methods) composables/ useZodForm.ts # Zod-backed reactive form: validate, reset, isDirty, error clearing useLoading.ts # Loading state wrapper: withLoading(async fn) useAction.ts # Async action helper: action(fn), loading, error, data refs useAuth.ts # (legacy — AuthService class is the canonical auth module) useLoginForm.ts # Login form composable using useAction usePrestations.ts # (legacy — use services + composables directly in views) useInvoices.ts # (legacy — use services + composables directly in views) useContactForm.ts # (legacy — use services + composables directly in views) useExamples.ts # (legacy — use services + composables directly in views) components/ common/ AppBar.vue # Top app bar with navigation links + user info + logout ConfirmDialog.vue # Generic confirmation dialog (yes/no) GenericForm.vue # Form wrapper: slots form fields, handles validate/cancel/isDirty PaginatedTable.vue # Paginated data table wrapper around v-data-table-server <resource>/ <Resource>List.vue # List view: delegates to PaginatedTable, defines headers, custom cell slots <Resource>Form.vue # Form fields: uses GenericForm slot, maps form/errors to v-text-field etc. <Resource>Modal.vue # Dialog wrapper: manages dialog state, initial data, cancel/save events <Resource>Drawer.vue # (optional) Slide-out panel for view/edit details <Resource>Autocomplete.vue # (optional) Search-based autocomplete component views/ LoginView.vue # Login page <Resource>View.vue # Resource CRUD view: list, search, create, edit, delete orchestration directives/ permission.ts # v-permission directive: hides elements based on user role ``` --- ## Core Principle: Separation of Concerns **Services** handle HTTP communication. **Composables** encapsulate reactive state and business logic. **Views** orchestrate services and composables. **Components** handle UI rendering via slots. ### The Four Layers | Layer | Responsibility | Pattern | |-------|---------------|---------| | **Service** | HTTP requests to backend, typed return values | Static class methods wrapping `ApiService` | | **Composable** | Reactive state, form validation, async actions | `function useXxx()` returning reactive refs + functions | | **View** | Page-level orchestration: fetch data, wire actions | `<script setup>` calling services + composables | | **Component** | UI rendering, slot-based customization | `<script setup>` + `<script generic>` for typed generics | --- ## Service Layer ### API Service (`services/api.service.ts`) The generic HTTP client. All other services use it. ```ts const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; // Core request function async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> // Methods ApiService.get<T>(endpoint) ApiService.getBlob(endpoint) // returns Blob (for file downloads) ApiService.paginate<T>(endpoint, params) // adds PaginationInput query params ApiService.post<T>(endpoint, body) ApiService.put<T>(endpoint, body) ApiService.delete<T>(endpoint) ``` ### Resource Service Pattern Each resource gets its own service class with static methods. **Always** import types from `@chdev/common`. ```ts import { ApiService } from './api.service'; import type { Client, CreateClientInput, PaginatedResponse, PaginationInput, UpdateClientInput, } from '@chdev/common'; export class ClientService { static getAll() { return ApiService.get<Array<Client>>('/clients'); } static getAllPaginated(params: PaginationInput) { return ApiService.paginate<PaginatedResponse<Client>>('/clients/paginated', params); } static getById(id: number) { return ApiService.get<Client>(`/clients/${id}`); } static create(data: CreateClientInput) { return ApiService.post<Client>('/clients', data); } static update(id: number, data: UpdateClientInput) { return ApiService.put<Client>(`/clients/${id}`, data); } static delete(id: number) { return ApiService.delete(`/clients/${id}`); } } ``` **Rules:** - Use `ApiService.get<T>()` for full lists (unpaginated, for autocomplete/selects) - Use `ApiService.paginate<T>()` with `PaginatedResponse<T>` for list views - Always use `withLoading()` from `useLoading` composable around async calls in views - Import types via `import type { ... }` from `@chdev/common` --- ## Composable Layer ### useLoading (`composables/useLoading.ts`) Lightweight loading wrapper. Provides a boolean `isLoading` ref and a `withLoading` helper. ```ts import { useLoading } from '@/composables/useLoading'; const { isLoading, withLoading } = useLoading(); // Usage async function fetchData() { const result = await withLoading(MyService.getAll()); // isLoading is true while the promise resolves } ``` ### useAction (`composables/useAction.ts`) Async action helper with loading, error, and data refs. Generic over return type with optional mapping. ```ts import { useAction } from '@/composables/useAction'; const { action, loading, error, data, clearError } = useAction( (args) => MyService.doSomething(args), { map: (res) => res as CustomType } // optional: transform response ); // In template: {{ action(someArg) }} or {{ loading.value }} ``` ### useZodForm (`composables/useZodForm.ts`) Zod-backed reactive form. Manages form state, validation, errors, and dirty tracking. ```ts import { useZodForm } from '@/composables/useZodForm'; import { createClientSchema } from '@chdev/common'; const { form, errors, validate, reset, isDirty } = useZodForm( createClientSchema, { companyName: '', email: '', phone: '', address: '', zipCode: 0, city: '', country: '', notes: '' } ); const result = validate(); if (result.success) { // result.data is fully typed z.output<typeof schema> } else { // result.errors is Record<string, string> } ``` **Rules:** - `form` is a Vue `reactive` object — bind directly with `v-model="form.fieldName"` - `errors` is a flat `Record<string, string>` keyed by field name - Error auto-resets when the user modifies the field - `isDirty` computed ref tracks whether form was modified from initial state - `reset()` reverts to the original values - For nested schema errors (e.g., `lines.0.prestationId`), the dot-notation key is preserved ### Legacy Composables Composables like `useAuth`, `useLoginForm`, `useInvoices`, `usePrestations`, `useContactForm`, `useExamples` exist but are **legacy patterns** using manual refs with manual loading/error handling. The modern approach is: - **Auth**: Use `AuthService` class directly in views - **Forms**: Use `useZodForm` + `GenericForm` component - **Async operations**: Use `useLoading` + `useAction` or inline `withLoading` in views - **Data fetching**: Handle directly in the view with `onMounted` + service calls **Do not create new composables that manually manage loading/error refs.** Use the building blocks above. --- ## View Layer ### View Pattern Views orchestrate services, composables, and child components. They define page-level state and handle CRUD operations. ```vue <template> <v-container fluid> <v-row class="align-center"> <v-col><h1 class="text-h4">{{ pageTitle }}</h1></v-col> <v-col><v-text-field clearable v-model="search" placeholder="Rechercher..." /></v-col> <v-col class="text-right"> <v-btn color="primary" @click="startCreate">Créer un {{ singularName }}</v-btn> </v-col> </v-row> <v-row> <v-col> <ResourceList v-model:options="options" v-model:row-selected="rowSelected" :is-loading="isLoading" :items="items" :items-length="totalItems" @delete="deleteItem" @edit="startEdit" /> </v-col> </v-row> <ResourceModal v-model="modalOpen" :item="selectedItem" :is-creating="isCreating" :schema="schema" @cancel="handleCancel" @saved="handleSaved" /> </v-container> </template> <script lang="ts" setup> import { onMounted, ref, watch, computed } from 'vue'; import { useLoading } from '@/composables/useLoading'; import { ResourceService } from '@/services/resource.service'; import { createResourceSchema, updateResourceSchema } from '@chdev/common'; import ResourceList from '@/components/<resource>/<Resource>List.vue'; import ResourceModal from '@/components/<resource>/<Resource>Modal.vue'; import type { Item, CreateItemInput, PaginationInput, UpdateItemInput } from '@chdev/common'; const { isLoading, withLoading } = useLoading(); const items = ref<Array<Item>>([]); const rowSelected = ref<Item | null>(null); const selectedItem = ref<Item | null>(null); const search = ref<string>(''); const totalItems = ref<number>(0); const modalOpen = ref<boolean>(false); const isCreating = ref<boolean>(false); const options = ref<PaginationInput>({ search: '', totalItems: 0, page: 1, itemsPerPage: 10, sortBy: undefined, sortDesc: false, }); watch(options, (newOptions) => fetchItems(newOptions)); watch(search, (newSearch) => { options.value = { ...options.value, search: newSearch || '' }; }); const schema = computed(() => isCreating.value ? createResourceSchema : updateResourceSchema); async function fetchItems(pagination: PaginationInput) { const { data, total } = await withLoading(ResourceService.getAllPaginated(pagination)); items.value = data; totalItems.value = total; } async function deleteItem(item: Item) { await withLoading(ResourceService.delete(item.id)); fetchItems(options.value); } function startCreate() { isCreating.value = true; selectedItem.value = null; modalOpen.value = true; } function startEdit(item: Item) { isCreating.value = false; selectedItem.value = item; modalOpen.value = true; } function handleCancel() { selectedItem.value = null; isCreating.value = false; modalOpen.value = false; } async function handleSaved(value: CreateItemInput | UpdateItemInput) { if ('id' in value) { await withLoading(ResourceService.update(value.id, value)); } else { await withLoading(ResourceService.create(value)); } handleCancel(); fetchItems(options.value); } onMounted(() => fetchItems(options.value)); </script> ``` **Rules:** - Use `v-container fluid` as the outer wrapper - Search + title + create button in a single `v-row` - List component in a separate `v-row` - Modal/Drawer component bound after the list - Always use `useLoading` + `withLoading` for async operations - `PaginationInput` type from `@chdev/common` for pagination state - `watch(options, ...)` triggers fetch on page/sort/items-per-page changes - `watch(search, ...)` updates pagination options on search input change - Schema switches between create/update based on `isCreating` computed - `onMounted` fetches initial data
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub