@innobdx/nuxt-crud
Nuxt CRUD Library
Une bibliothèque Nuxt 3 pour faciliter la création d'écran de gestion pour vos entités (Create, Read, Update, Delete).
Installation
npm install @innobdx/nuxt-crud
Configuration
Ajoutez le module à votre application Nuxt dans nuxt.config.ts:
export default defineNuxtConfig({
modules: [
'@innobdx/nuxt-crud'
],
// Vous pouvez configurer les options du module ici
crud: {
// Options de base du module
isEnabled: true,
prefix: 'Crud',
customFetchKey: 'useApiFetch', // Optionnel: nom de votre fonction fetch personnalisée
// Configuration personnalisée pour le CRUD
config: {
editMode: 'DIALOG', // Mode d'édition ('DIALOG' ou autre mode supporté)
idRegex: /^\d+$/,
route: {
create: 'create',
update: 'update',
view: 'view'
},
subtitle: {
list: 'Liste',
create: 'Création',
update: 'Modification',
view: 'Détails'
},
method: {
create: 'POST',
update: 'PATCH'
},
textValidButton: {
create: 'AJOUTER',
update: 'MODIFIER'
},
authorizedEntities: [], // Tableau vide pour autoriser toutes les entités
dataTableOptions: {
serverSidePagination: true,
serverSidePaginationOptions: {
page: { name: 'page', value: 1 },
limit: { name: 'size', value: 10 },
sort: { name: 'sort', value: 'id' },
query: { name: 'query', value: '' }
},
itemsPerPageOptions: [
{ value: 10, title: '10' },
{ value: 25, title: '25' },
{ value: 50, title: '50' }
],
loadingText: 'Chargement en cours...'
}
}
}
})
Configuration des requêtes API et Authentification
La bibliothèque utilise le système de fetch standard de Nuxt et ne gère pas les URLs de base ou l'authentification. C'est à vous de configurer ces aspects dans votre application.
Configuration avec une instance de fetch personnalisée
Le module supporte l'utilisation d'une instance de fetch personnalisée pour tous ses appels API. Cela vous permet de configurer votre propre instance avec une URL de base et des en-têtes d'authentification sans affecter les autres requêtes Nuxt.
Pour configurer cette fonctionnalité:
- Définir une URL de base et configurer customFetchKey dans
nuxt.config.ts:
export default defineNuxtConfig({
modules: [
'@innobdx/nuxt-crud'
],
// Indiquer au module quelle instance de fetch personnalisée utiliser
crud: {
customFetchKey: '$apiFetch' // Nom de l'instance de fetch fournie par votre plugin
},
// Configuration de l'URL de base pour vos API
runtimeConfig: {
public: {
apiBaseUrl: 'http://localhost:3000/api' // URL de base de votre API
}
}
})
- Créer un plugin pour fournir votre instance de fetch personnalisée:
// plugins/apiFetch.ts
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const { $auth } = useNuxtApp() // Si vous utilisez un plugin d'authentification
// Création d'une instance de fetch dédiée aux appels API
const apiFetch = $fetch.create({
baseURL: config.public.apiBaseUrl,
headers: {
'Content-Type': 'application/json',
// Ajoutez vos en-têtes d'authentification
'Authorization': `Bearer ${$auth?.getAccessToken?.() || ''}`
}
})
// Exposer l'instance de fetch pour qu'elle soit accessible
return {
provide: {
apiFetch // Sera disponible comme $apiFetch
}
}
})
Cette approche présente plusieurs avantages:
- Ne modifie pas le fetch global de Nuxt, donc n'affecte pas les autres requêtes système
- Permet une séparation claire entre les requêtes API et les autres requêtes
- Facilite la gestion des en-têtes d'authentification de manière isolée
- Respecte le principe de séparation des responsabilités
Le module utilisera automatiquement votre instance personnalisée pour tous ses appels API internes, sans avoir à modifier son code source.
Création d'un nouveau domaine
Pour faciliter la création de nouveaux domaines, la bibliothèque inclut un script CLI interactif. Pour l'utiliser:
# Option 1: Utiliser npx
npx makecrud
# Option 2: Ajouter un script dans votre package.json
# "scripts": {
# "make:crud": "makecrud"
# }
# Puis exécuter:
npm run make:crud
Le script vous guidera à travers la création d'un nouveau domaine en vous demandant:
- Le nom de l'entité
- L'endpoint API
- Le nom de la propriété ID
- Les titres à utiliser dans l'interface
Le script génère automatiquement la structure de dossiers et tous les fichiers nécessaires.
Structure du projet
Pour que la bibliothèque fonctionne correctement, vous devez respecter une structure spécifique avec un répertoire par entité:
📁 votre-projet/
┣ 📁 domains/
┃ ┣ 📁 product/
┃ ┃ ┣ 📄 index.ts # Exporte les éléments du domaine
┃ ┃ ┣ 📄 FormProduct.vue # Composant de formulaire pour l'entité
┃ ┃ ┣ 📄 dataTableTemplates.const.ts # Templates pour les cellules du tableau
┃ ┃ ┣ 📄 defaultValue.const.ts # Valeurs par défaut pour l'entité
┃ ┃ ┣ 📄 tableHeaders.const.ts # En-têtes du tableau
┃ ┃ ┗ 📄 titles.const.ts # Titres pour l'interface
┃ ┣ 📁 user/
┃ ┃ ┣ 📄 index.ts
┃ ┃ ┣ 📄 FormUser.vue
┃ ┃ ┣ 📄 dataTableTemplates.const.ts
┃ ┃ ┣ 📄 defaultValue.const.ts
┃ ┃ ┣ 📄 tableHeaders.const.ts
┃ ┃ ┗ 📄 titles.const.ts
┣ 📄 domains.ts # Fichier qui exporte tous les domaines
┣ 📁 ...
Utilisation
1. Définissez vos domaines d'entités
Pour chaque entité, vous devez créer un dossier dans domains/ avec la structure décrite ci-dessus.
la commande makecrud le fait pour vous sinon vous pouvez vous inspirez du playbook qui démontre la configuration d'un domaine utilisateur.
2. Navigation automatique
La bibliothèque génère automatiquement les routes CRUD pour vos entités, aucune page n'a besoin d'être créé.
Domaines « select-only »
Un dossier de domains/ peut n'exporter qu'un CrudSelectDomain (littéral typé, sans titles,
tableHeaders ni defaultValue) destiné uniquement à alimenter un CrudSelect. Ces domaines ne
sont pas routables : seuls ceux construits avec defineCrudDomain() sont injectés dans
$crudDomains et servis par les routes /:entity et /:entity/:action/:id?. Naviguer vers
l'endpoint d'un domaine select-only produit une erreur explicite Unknown domain "…" plutôt qu'un
crash de rendu.
Le marqueur est posé par defineCrudDomain() et vérifiable via isCrudDomain(domain).
Interface utilisateur avec Vuetify
La bibliothèque utilise Vuetify pour l'interface utilisateur. Les composants CRUD s'intègrent automatiquement avec votre configuration Vuetify existante.
CrudSelect
CrudSelect charge ses options depuis le domaine passé directement au composant avec la props domain, sans declarer de sous-domaine et sans dependance au domaine courant. Ce domaine peut etre un domaine CRUD complet declaré avec defineCrudDomain<T>(), ou un CrudSelectDomain minimal reservé aux selects.
<script setup lang="ts">
import type { UserInterface as User } from '~/types/user'
import { UsersDomain } from '~/domains/users'
const selectedUser = ref<User | null>(null)
function getUserTitle(user: User) {
return `${user.firstName} ${user.lastName}`
}
</script>
<template>
<CrudSelect
v-model="selectedUser"
:domain="UsersDomain"
label="Utilisateur"
density="compact"
variant="solo"
:item-title="getUserTitle"
:selection-title="getUserTitle"
/>
</template>
Le domaine lie reste un domaine CRUD normal :
export const UsersDomain = defineCrudDomain<User>()('id', {
name: 'users',
endpoint: 'users',
responseFormat: 'envelope',
searchEndpoint: 'search/findByFirstNameContainingIgnoreCaseOrLastNameContainingIgnoreCase',
searchParam: ['firstName', 'lastName'],
// ...
})
Pour une relation, le domaine parent ne declare rien de special. Il suffit que le domaine du type cible existe :
export const ProductDomain = defineCrudDomain<Product>()('id', {
name: 'products',
endpoint: 'products',
responseFormat: 'envelope',
searchEndpoint: 'search/findByNameContainingIgnoreCase',
searchParam: 'name',
// ...
})
Dans le formulaire utilisateur, le select cible explicitement le type voulu. Le mode multiple reste pilote par la prop multiple :
<CrudSelect
v-model="user.product"
:domain="ProductDomain"
:item-title="product => product.name"
:selection-title="product => product.name"
/>
<CrudSelect
v-model="user.products"
:domain="ProductDomain"
multiple
:item-title="product => product.name"
:selection-title="product => product.name"
/>
domain est le domaine CRUD utilise pour charger les options du select. Il n'y a pas de fallback par nom de domaine, sous-domaine, attribut parent, type ou valeur selectionnee.
Pour une recherche serveur, il faut configurer explicitement searchEndpoint et searchParam, ou passer searchAttributes sur le composant, ou fournir customCrud.readAll. Sans configuration de recherche, saisir du texte leve une erreur explicite. Avec searchParam: string[], la meme recherche est envoyee a chaque parametre configure.
Les attributs Vuetify publics (variant, density, class, style, etc.) sont transmis au VAutocomplete. Les bindings internes (items, loading, item-title, item-value, value-comparator, disabled, search, menu) restent pilotes par CrudSelect.
Slots disponibles :
<CrudSelect
v-model="selectedUser"
:domain="UsersDomain"
:item-title="getUserTitle"
:selection-title="getUserTitle"
>
<template #item-prepend="{ item }">
<UserAvatar :user="item" />
</template>
<template #item="{ props, item, title, subtitle }">
<VListItem v-bind="props" :title="title" :subtitle="subtitle" />
</template>
<template #item-append="{ item }">
<UserStatus :user="item" />
</template>
<template #selection-prepend="{ item }">
<UserAvatar :user="item" />
</template>
<template #selection="{ item }">
{{ item.firstName }} {{ item.lastName }}
</template>
<template #selection-append="{ item }">
<UserStatus :user="item" />
</template>
</CrudSelect>
CrudSelect utilise les labels i18n du domaine/module pour noDataEmpty, noDataError et loadingText. La prop noData reste disponible pour surcharger localement le texte vide.
Actions personnalisées
Pour ajouter des actions personnalisées, vous pouvez étendre les templates de colonnes dans votre configuration de domaine.
Composants disponibles
AppCrudReadAllView: Vue pour afficher la liste des entitésAppCrudActionView: Vue pour les actions (créer/éditer/voir les détails)AppCrudTable: Tableau pour afficher les entitésAppCrudTableActions: Boutons d'action pour chaque ligneAppCrudTableDeleteDialog: Dialogue de confirmation de suppressionAppCrudTableNoData: Affichage lorsqu'il n'y a pas de donnéesAppCrudTableTitle: Titre du tableau avec boutons d'actionAppChip: Composant pour afficher des badges/étiquettes
Composables disponibles
useListEntityService: Gère la récupération et la pagination des entitésuseFetchService: Service de base pour les requêtes APIuseCrudFieldError(path): Erreurs serveur ciblées sur un champ (voir « Gestion des erreurs »)useCrudFieldErrorMessage(path): Premier message d'erreur pour un champuseCrudFieldErrorMessages(path): Liste de messages, prête à brancher sur Vuetify:error-messages
Gestion des erreurs
Les erreurs réseau (FetchError ofetch) sont normalisées automatiquement en CrudNetworkError typé. Plus besoin de configurer un chemin (errorPath) pour extraire le message : le module reconnaît les conventions courantes (RFC 7807, Spring, Laravel, payloads ad-hoc).
Affichage par défaut
Une erreur de soumission affiche un <v-alert role="alert" aria-live="polite"> avec error.message. Le slot #error de CrudActionView / CrudFormLayout permet de surcharger l'affichage :
<CrudActionView :domain="UserDomain" :item="user" action="create">
<template #error="{ error, fieldErrors }">
<v-alert type="error" :text="error.message" />
<ul v-if="fieldErrors.length">
<li v-for="(fe, i) in fieldErrors" :key="i">{{ fe.path.join('.') }}: {{ fe.message }}</li>
</ul>
</template>
</CrudActionView>
Vous pouvez aussi définir errorComponent sur le domaine pour fournir un composant réutilisable.
Erreurs par champ (server-side validation)
Si le backend renvoie un 422 au format Spring/Laravel :
{ "message": "Validation failed", "errors": { "username": ["This username already exists"] } }
…ou au format RFC 7807 (errors: [{ pointer: "/username", detail: "..." }]), les erreurs sont automatiquement converties en FieldError[] et exposées au composant de formulaire via useCrudFieldError :
<script setup lang="ts">
import { useCrudFieldErrorMessages } from '#crud/composables/useCrudFieldError'
const usernameServerErrors = useCrudFieldErrorMessages('username')
// ou : useCrudFieldErrorMessages(['address', 'zip'])
// ou : useCrudFieldErrorMessages('items[0].name')
</script>
<template>
<VTextField
v-model="user.username"
label="Username"
:error-messages="usernameServerErrors"
/>
</template>
Les erreurs Zod issues du schema du domaine sont également converties en FieldError[] et propagées via le même mécanisme.
Format FieldError
interface FieldError {
path: (string | number)[] // ex: ['address', 'zip'], ['items', 0, 'name']
message: string
code?: string // optionnel (Zod issue.code, RFC 7807 code, ...)
}
Compatible Standard Schema.
Backends non-standard : errorAdapter
Pour un backend exotique, fournissez un adapter via un plugin Nuxt — il sera prioritaire sur les heuristiques internes :
// plugins/crud-error-adapter.ts
import type { ErrorAdapter } from '@innobdx/nuxt-crud'
export default defineNuxtPlugin(() => ({
provide: {
crudErrorAdapter: ((err: any) => {
if (err?.data?.legacyError) {
return {
message: err.data.legacyError.userMessage,
status: err.statusCode,
fieldErrors: err.data.legacyError.fields?.map((f: any) => ({
path: f.field.split('.'),
message: f.reason,
})),
}
}
return undefined // fallback aux heuristiques internes
}) satisfies ErrorAdapter,
},
}))
Hiérarchie d'erreurs exposées
| Classe | Cas |
|---|---|
CrudError |
Base — domain, cause, fieldErrors? |
CrudNetworkError |
Erreur HTTP — status, data, fieldErrors? |
CrudValidationError |
Échec Zod côté client — zodError, fieldErrors |
CrudNotFoundError |
404 entité — id |
Tous sont exportés depuis @innobdx/nuxt-crud.
Licence
MIT
Related Packages
Version History
| Version | Released | License |
|---|---|---|
| 1.30.0 | MIT | |
| 1.29.0 | MIT | |
| 1.28.0 | MIT | |
| 1.27.0 | MIT | |
| 1.25.0 | MIT | |
| 1.23.0 | MIT | |
| 1.22.0 | MIT | |
| 1.21.0 | MIT | |
| 1.20.0 | MIT | |
| 1.19.0 | MIT | |
| 1.18.0 | MIT | |
| 1.17.0 | MIT | |
| 1.16.0 | MIT | |
| 1.15.0 | MIT | |
| 1.14.0 | MIT | |
| 1.13.0 | MIT | |
| 1.12.0 | MIT | |
| 1.11.0 | MIT | |
| 1.10.0 | MIT | |
| 1.9.0 | MIT | |
| 1.8.0 | MIT | |
| 1.7.0 | MIT | |
| 1.6.0 | MIT | |
| 1.5.0 | MIT | |
| 1.4.0 | MIT | |
| 1.3.0 | MIT | |
| 1.2.0 | MIT |