Creer une Application Web Full-Stack avec Nuxt 4 et Vue 3

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Ce tutoriel vous guide pas a pas dans la creation d'une application web full-stack avec Nuxt 4 et Vue 3. Vous allez construire une application de gestion de taches (task manager) avec authentification, API routes, et une base de donnees PostgreSQL via Prisma. A la fin, vous aurez une application complete prete pour la production.

Objectifs d'apprentissage

A la fin de ce tutoriel, vous serez capable de :

  • Creer et configurer un projet Nuxt 4 avec TypeScript
  • Maitriser le systeme de routage base sur les fichiers de Nuxt
  • Construire des composants Vue 3 reactifs avec la Composition API
  • Creer des API routes serveur avec Nitro
  • Integrer Prisma ORM pour la gestion de la base de donnees
  • Implementer une authentification simple avec les sessions
  • Deployer votre application en production

Prerequisites

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installe sur votre machine
  • pnpm (gestionnaire de paquets recommande pour Nuxt)
  • PostgreSQL installe localement ou un service cloud (Neon, Supabase)
  • Connaissances de base en JavaScript/TypeScript
  • Familiarite avec les concepts de base de Vue.js (composants, reactivite)
  • Un editeur de code (VS Code avec l'extension Volar recommandee)

Ce que vous allez construire

Une application TaskFlow — un gestionnaire de taches full-stack avec :

  • Inscription et connexion des utilisateurs
  • Creation, modification et suppression de taches
  • Filtrage par statut (a faire, en cours, termine)
  • Interface responsive avec le design system de Nuxt UI
  • API REST securisee cote serveur

Etape 1 : Initialiser le projet Nuxt 4

Commencez par creer un nouveau projet Nuxt 4 :

pnpm dlx nuxi@latest init taskflow-app
cd taskflow-app

Quand le CLI vous demande les options, selectionnez :

  • Package manager : pnpm
  • Initialize git : Oui

Ensuite, installez les dependances et lancez le serveur de developpement :

pnpm install
pnpm dev

Votre application est accessible sur http://localhost:3000.

Structure du projet

Voici la structure de base de votre projet Nuxt 4 :

taskflow-app/
├── app/
│   ├── components/     # Composants Vue reutilisables
│   ├── composables/    # Logique reutilisable (hooks)
│   ├── layouts/        # Layouts de pages
│   ├── pages/          # Pages (routage automatique)
│   └── app.vue         # Composant racine
├── server/
│   ├── api/            # API routes
│   ├── middleware/      # Middleware serveur
│   └── utils/          # Utilitaires serveur
├── prisma/
│   └── schema.prisma   # Schema de base de donnees
├── nuxt.config.ts      # Configuration Nuxt
├── package.json
└── tsconfig.json

Nuxt 4 adopte une nouvelle structure de repertoires avec le dossier app/ qui contient tout le code client. Cette separation claire entre client (app/) et serveur (server/) ameliore l'organisation du code.

Etape 2 : Configurer Nuxt 4

Mettez a jour votre fichier nuxt.config.ts avec les modules necessaires :

// nuxt.config.ts
export default defineNuxtConfig({
  future: {
    compatibilityVersion: 4,
  },
 
  devtools: { enabled: true },
 
  modules: [
    '@nuxt/ui',
    '@nuxt/fonts',
  ],
 
  runtimeConfig: {
    sessionSecret: process.env.SESSION_SECRET || 'dev-secret-change-in-production',
    databaseUrl: process.env.DATABASE_URL,
    public: {
      appName: 'TaskFlow',
    },
  },
 
  compatibilityDate: '2026-03-01',
})

Installez les modules Nuxt UI et Fonts :

pnpm add @nuxt/ui @nuxt/fonts

Creez le fichier .env a la racine du projet :

DATABASE_URL="postgresql://user:password@localhost:5432/taskflow"
SESSION_SECRET="votre-secret-super-securise-ici"

Etape 3 : Configurer Prisma et la base de donnees

Installez Prisma et initialisez-le :

pnpm add -D prisma
pnpm add @prisma/client
pnpm dlx prisma init

Definissez votre schema de base de donnees dans prisma/schema.prisma :

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  password  String
  tasks     Task[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
 
model Task {
  id          String     @id @default(cuid())
  title       String
  description String?
  status      TaskStatus @default(TODO)
  priority    Priority   @default(MEDIUM)
  userId      String
  user        User       @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt
}
 
enum TaskStatus {
  TODO
  IN_PROGRESS
  DONE
}
 
enum Priority {
  LOW
  MEDIUM
  HIGH
}

Appliquez les migrations :

pnpm dlx prisma migrate dev --name init

Creez un utilitaire serveur pour acceder au client Prisma :

// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client'
 
const prisma = new PrismaClient()
 
export default prisma

Etape 4 : Creer les API routes

Nuxt utilise le moteur Nitro pour les routes serveur. Creez les endpoints de votre API.

Route d'inscription

// server/api/auth/register.post.ts
import bcrypt from 'bcryptjs'
import prisma from '~/server/utils/prisma'
 
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
 
  if (!body.email || !body.password || !body.name) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email, nom et mot de passe requis',
    })
  }
 
  const existingUser = await prisma.user.findUnique({
    where: { email: body.email },
  })
 
  if (existingUser) {
    throw createError({
      statusCode: 409,
      statusMessage: 'Un compte avec cet email existe deja',
    })
  }
 
  const hashedPassword = await bcrypt.hash(body.password, 12)
 
  const user = await prisma.user.create({
    data: {
      email: body.email,
      name: body.name,
      password: hashedPassword,
    },
    select: {
      id: true,
      email: true,
      name: true,
    },
  })
 
  // Stocker l'utilisateur en session
  const session = await useSession(event, {
    password: useRuntimeConfig().sessionSecret,
  })
  await session.update({ userId: user.id })
 
  return user
})

Route de connexion

// server/api/auth/login.post.ts
import bcrypt from 'bcryptjs'
import prisma from '~/server/utils/prisma'
 
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
 
  if (!body.email || !body.password) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email et mot de passe requis',
    })
  }
 
  const user = await prisma.user.findUnique({
    where: { email: body.email },
  })
 
  if (!user || !(await bcrypt.compare(body.password, user.password))) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Identifiants invalides',
    })
  }
 
  const session = await useSession(event, {
    password: useRuntimeConfig().sessionSecret,
  })
  await session.update({ userId: user.id })
 
  return {
    id: user.id,
    email: user.email,
    name: user.name,
  }
})

Middleware d'authentification serveur

// server/middleware/auth.ts
import prisma from '~/server/utils/prisma'
 
export default defineEventHandler(async (event) => {
  const protectedRoutes = ['/api/tasks']
 
  const isProtected = protectedRoutes.some((route) =>
    event.path?.startsWith(route)
  )
 
  if (!isProtected) return
 
  const session = await useSession(event, {
    password: useRuntimeConfig().sessionSecret,
  })
 
  if (!session.data?.userId) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Non authentifie',
    })
  }
 
  const user = await prisma.user.findUnique({
    where: { id: session.data.userId as string },
    select: { id: true, email: true, name: true },
  })
 
  if (!user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Utilisateur introuvable',
    })
  }
 
  event.context.user = user
})

CRUD des taches

// server/api/tasks/index.get.ts
import prisma from '~/server/utils/prisma'
 
export default defineEventHandler(async (event) => {
  const user = event.context.user
  const query = getQuery(event)
 
  const where: any = { userId: user.id }
 
  if (query.status) {
    where.status = query.status
  }
 
  const tasks = await prisma.task.findMany({
    where,
    orderBy: { createdAt: 'desc' },
  })
 
  return tasks
})
// server/api/tasks/index.post.ts
import prisma from '~/server/utils/prisma'
 
export default defineEventHandler(async (event) => {
  const user = event.context.user
  const body = await readBody(event)
 
  if (!body.title) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Le titre est requis',
    })
  }
 
  const task = await prisma.task.create({
    data: {
      title: body.title,
      description: body.description || null,
      priority: body.priority || 'MEDIUM',
      userId: user.id,
    },
  })
 
  return task
})
// server/api/tasks/[id].patch.ts
import prisma from '~/server/utils/prisma'
 
export default defineEventHandler(async (event) => {
  const user = event.context.user
  const id = getRouterParam(event, 'id')
  const body = await readBody(event)
 
  const task = await prisma.task.findFirst({
    where: { id, userId: user.id },
  })
 
  if (!task) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Tache introuvable',
    })
  }
 
  const updated = await prisma.task.update({
    where: { id },
    data: {
      title: body.title ?? task.title,
      description: body.description ?? task.description,
      status: body.status ?? task.status,
      priority: body.priority ?? task.priority,
    },
  })
 
  return updated
})
// server/api/tasks/[id].delete.ts
import prisma from '~/server/utils/prisma'
 
export default defineEventHandler(async (event) => {
  const user = event.context.user
  const id = getRouterParam(event, 'id')
 
  const task = await prisma.task.findFirst({
    where: { id, userId: user.id },
  })
 
  if (!task) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Tache introuvable',
    })
  }
 
  await prisma.task.delete({ where: { id } })
 
  return { success: true }
})

Installez bcryptjs pour le hashage des mots de passe :

pnpm add bcryptjs
pnpm add -D @types/bcryptjs

Etape 5 : Creer le composable d'authentification

Creez un composable pour gerer l'etat d'authentification cote client :

// app/composables/useAuth.ts
interface User {
  id: string
  email: string
  name: string
}
 
export function useAuth() {
  const user = useState<User | null>('auth-user', () => null)
  const isAuthenticated = computed(() => !!user.value)
 
  async function login(email: string, password: string) {
    const data = await $fetch<User>('/api/auth/login', {
      method: 'POST',
      body: { email, password },
    })
    user.value = data
    return data
  }
 
  async function register(name: string, email: string, password: string) {
    const data = await $fetch<User>('/api/auth/register', {
      method: 'POST',
      body: { name, email, password },
    })
    user.value = data
    return data
  }
 
  async function logout() {
    await $fetch('/api/auth/logout', { method: 'POST' })
    user.value = null
    navigateTo('/login')
  }
 
  async function fetchUser() {
    try {
      const data = await $fetch<User>('/api/auth/me')
      user.value = data
    } catch {
      user.value = null
    }
  }
 
  return {
    user,
    isAuthenticated,
    login,
    register,
    logout,
    fetchUser,
  }
}

Ajoutez les routes manquantes pour la session :

// server/api/auth/me.get.ts
export default defineEventHandler(async (event) => {
  const user = event.context.user
  if (!user) {
    throw createError({ statusCode: 401, statusMessage: 'Non authentifie' })
  }
  return user
})
// server/api/auth/logout.post.ts
export default defineEventHandler(async (event) => {
  const session = await useSession(event, {
    password: useRuntimeConfig().sessionSecret,
  })
  await session.clear()
  return { success: true }
})

Etape 6 : Creer le layout principal

Definissez un layout avec une barre de navigation :

<!-- app/layouts/default.vue -->
<script setup lang="ts">
const { user, isAuthenticated, logout } = useAuth()
const config = useRuntimeConfig()
</script>
 
<template>
  <div class="min-h-screen bg-gray-50 dark:bg-gray-900">
    <header class="bg-white dark:bg-gray-800 shadow-sm">
      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between items-center h-16">
          <NuxtLink to="/" class="text-xl font-bold text-primary">
            {{ config.public.appName }}
          </NuxtLink>
 
          <nav class="flex items-center gap-4">
            <template v-if="isAuthenticated">
              <span class="text-sm text-gray-600 dark:text-gray-300">
                {{ user?.name }}
              </span>
              <UButton
                variant="ghost"
                color="red"
                @click="logout"
              >
                Deconnexion
              </UButton>
            </template>
            <template v-else>
              <UButton to="/login" variant="ghost">Connexion</UButton>
              <UButton to="/register" color="primary">Inscription</UButton>
            </template>
          </nav>
        </div>
      </div>
    </header>
 
    <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
      <slot />
    </main>
  </div>
</template>

Etape 7 : Construire les pages

Page d'accueil

<!-- app/pages/index.vue -->
<script setup lang="ts">
const { isAuthenticated } = useAuth()
</script>
 
<template>
  <div class="text-center py-20">
    <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
      Gerez vos taches efficacement
    </h1>
    <p class="text-lg text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
      TaskFlow est un gestionnaire de taches simple et puissant.
      Organisez, priorisez et suivez l'avancement de vos projets.
    </p>
    <div class="flex gap-4 justify-center">
      <UButton
        v-if="!isAuthenticated"
        to="/register"
        size="lg"
        color="primary"
      >
        Commencer gratuitement
      </UButton>
      <UButton
        v-if="isAuthenticated"
        to="/dashboard"
        size="lg"
        color="primary"
      >
        Aller au tableau de bord
      </UButton>
    </div>
  </div>
</template>

Page de connexion

<!-- app/pages/login.vue -->
<script setup lang="ts">
definePageMeta({ layout: 'default' })
 
const { login } = useAuth()
const error = ref('')
const loading = ref(false)
 
const form = reactive({
  email: '',
  password: '',
})
 
async function handleSubmit() {
  error.value = ''
  loading.value = true
  try {
    await login(form.email, form.password)
    navigateTo('/dashboard')
  } catch (e: any) {
    error.value = e.data?.statusMessage || 'Erreur de connexion'
  } finally {
    loading.value = false
  }
}
</script>
 
<template>
  <div class="max-w-md mx-auto mt-16">
    <UCard>
      <template #header>
        <h2 class="text-2xl font-bold text-center">Connexion</h2>
      </template>
 
      <form @submit.prevent="handleSubmit" class="space-y-4">
        <UAlert
          v-if="error"
          color="red"
          :title="error"
          variant="subtle"
        />
 
        <UFormGroup label="Email">
          <UInput
            v-model="form.email"
            type="email"
            placeholder="votre@email.com"
            required
          />
        </UFormGroup>
 
        <UFormGroup label="Mot de passe">
          <UInput
            v-model="form.password"
            type="password"
            placeholder="Votre mot de passe"
            required
          />
        </UFormGroup>
 
        <UButton
          type="submit"
          block
          :loading="loading"
        >
          Se connecter
        </UButton>
      </form>
 
      <template #footer>
        <p class="text-center text-sm text-gray-500">
          Pas encore de compte ?
          <NuxtLink to="/register" class="text-primary font-medium">
            Inscrivez-vous
          </NuxtLink>
        </p>
      </template>
    </UCard>
  </div>
</template>

Page d'inscription

<!-- app/pages/register.vue -->
<script setup lang="ts">
definePageMeta({ layout: 'default' })
 
const { register } = useAuth()
const error = ref('')
const loading = ref(false)
 
const form = reactive({
  name: '',
  email: '',
  password: '',
  confirmPassword: '',
})
 
async function handleSubmit() {
  if (form.password !== form.confirmPassword) {
    error.value = 'Les mots de passe ne correspondent pas'
    return
  }
 
  error.value = ''
  loading.value = true
  try {
    await register(form.name, form.email, form.password)
    navigateTo('/dashboard')
  } catch (e: any) {
    error.value = e.data?.statusMessage || "Erreur lors de l'inscription"
  } finally {
    loading.value = false
  }
}
</script>
 
<template>
  <div class="max-w-md mx-auto mt-16">
    <UCard>
      <template #header>
        <h2 class="text-2xl font-bold text-center">Inscription</h2>
      </template>
 
      <form @submit.prevent="handleSubmit" class="space-y-4">
        <UAlert
          v-if="error"
          color="red"
          :title="error"
          variant="subtle"
        />
 
        <UFormGroup label="Nom">
          <UInput
            v-model="form.name"
            placeholder="Votre nom"
            required
          />
        </UFormGroup>
 
        <UFormGroup label="Email">
          <UInput
            v-model="form.email"
            type="email"
            placeholder="votre@email.com"
            required
          />
        </UFormGroup>
 
        <UFormGroup label="Mot de passe">
          <UInput
            v-model="form.password"
            type="password"
            placeholder="Minimum 8 caracteres"
            required
            minlength="8"
          />
        </UFormGroup>
 
        <UFormGroup label="Confirmer le mot de passe">
          <UInput
            v-model="form.confirmPassword"
            type="password"
            placeholder="Repetez le mot de passe"
            required
          />
        </UFormGroup>
 
        <UButton
          type="submit"
          block
          :loading="loading"
        >
          Creer mon compte
        </UButton>
      </form>
 
      <template #footer>
        <p class="text-center text-sm text-gray-500">
          Deja un compte ?
          <NuxtLink to="/login" class="text-primary font-medium">
            Connectez-vous
          </NuxtLink>
        </p>
      </template>
    </UCard>
  </div>
</template>

Page du tableau de bord

<!-- app/pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth',
})
 
interface Task {
  id: string
  title: string
  description: string | null
  status: 'TODO' | 'IN_PROGRESS' | 'DONE'
  priority: 'LOW' | 'MEDIUM' | 'HIGH'
  createdAt: string
}
 
const activeFilter = ref<string | null>(null)
 
const { data: tasks, refresh } = await useFetch<Task[]>('/api/tasks', {
  query: computed(() => ({
    status: activeFilter.value || undefined,
  })),
})
 
const showCreateModal = ref(false)
 
const newTask = reactive({
  title: '',
  description: '',
  priority: 'MEDIUM' as 'LOW' | 'MEDIUM' | 'HIGH',
})
 
async function createTask() {
  await $fetch('/api/tasks', {
    method: 'POST',
    body: {
      title: newTask.title,
      description: newTask.description || null,
      priority: newTask.priority,
    },
  })
  newTask.title = ''
  newTask.description = ''
  newTask.priority = 'MEDIUM'
  showCreateModal.value = false
  refresh()
}
 
async function updateTaskStatus(taskId: string, status: string) {
  await $fetch(`/api/tasks/${taskId}`, {
    method: 'PATCH',
    body: { status },
  })
  refresh()
}
 
async function deleteTask(taskId: string) {
  await $fetch(`/api/tasks/${taskId}`, {
    method: 'DELETE',
  })
  refresh()
}
 
const filters = [
  { label: 'Toutes', value: null },
  { label: 'A faire', value: 'TODO' },
  { label: 'En cours', value: 'IN_PROGRESS' },
  { label: 'Terminees', value: 'DONE' },
]
 
const priorityColors = {
  LOW: 'green',
  MEDIUM: 'yellow',
  HIGH: 'red',
} as const
 
const statusLabels = {
  TODO: 'A faire',
  IN_PROGRESS: 'En cours',
  DONE: 'Termine',
} as const
</script>
 
<template>
  <div>
    <div class="flex justify-between items-center mb-8">
      <h1 class="text-2xl font-bold text-gray-900 dark:text-white">
        Mes taches
      </h1>
      <UButton
        color="primary"
        icon="i-heroicons-plus"
        @click="showCreateModal = true"
      >
        Nouvelle tache
      </UButton>
    </div>
 
    <!-- Filtres -->
    <div class="flex gap-2 mb-6">
      <UButton
        v-for="filter in filters"
        :key="filter.label"
        :variant="activeFilter === filter.value ? 'solid' : 'ghost'"
        size="sm"
        @click="activeFilter = filter.value"
      >
        {{ filter.label }}
      </UButton>
    </div>
 
    <!-- Liste des taches -->
    <div class="space-y-3">
      <UCard
        v-for="task in tasks"
        :key="task.id"
      >
        <div class="flex items-start justify-between">
          <div class="flex-1">
            <div class="flex items-center gap-2 mb-1">
              <h3
                class="font-medium"
                :class="task.status === 'DONE' ? 'line-through text-gray-400' : ''"
              >
                {{ task.title }}
              </h3>
              <UBadge :color="priorityColors[task.priority]" size="xs">
                {{ task.priority }}
              </UBadge>
            </div>
            <p v-if="task.description" class="text-sm text-gray-500">
              {{ task.description }}
            </p>
          </div>
 
          <div class="flex items-center gap-2">
            <USelect
              :model-value="task.status"
              :options="[
                { label: 'A faire', value: 'TODO' },
                { label: 'En cours', value: 'IN_PROGRESS' },
                { label: 'Termine', value: 'DONE' },
              ]"
              size="sm"
              @update:model-value="updateTaskStatus(task.id, $event)"
            />
            <UButton
              icon="i-heroicons-trash"
              color="red"
              variant="ghost"
              size="sm"
              @click="deleteTask(task.id)"
            />
          </div>
        </div>
      </UCard>
 
      <div
        v-if="!tasks?.length"
        class="text-center py-12 text-gray-500"
      >
        <p class="text-lg mb-2">Aucune tache trouvee</p>
        <p class="text-sm">Cliquez sur "Nouvelle tache" pour commencer</p>
      </div>
    </div>
 
    <!-- Modal de creation -->
    <UModal v-model="showCreateModal">
      <UCard>
        <template #header>
          <h3 class="text-lg font-medium">Nouvelle tache</h3>
        </template>
 
        <form @submit.prevent="createTask" class="space-y-4">
          <UFormGroup label="Titre" required>
            <UInput
              v-model="newTask.title"
              placeholder="Titre de la tache"
              required
            />
          </UFormGroup>
 
          <UFormGroup label="Description">
            <UTextarea
              v-model="newTask.description"
              placeholder="Description optionnelle"
            />
          </UFormGroup>
 
          <UFormGroup label="Priorite">
            <USelect
              v-model="newTask.priority"
              :options="[
                { label: 'Basse', value: 'LOW' },
                { label: 'Moyenne', value: 'MEDIUM' },
                { label: 'Haute', value: 'HIGH' },
              ]"
            />
          </UFormGroup>
 
          <div class="flex justify-end gap-2">
            <UButton
              variant="ghost"
              @click="showCreateModal = false"
            >
              Annuler
            </UButton>
            <UButton type="submit" color="primary">
              Creer
            </UButton>
          </div>
        </form>
      </UCard>
    </UModal>
  </div>
</template>

Etape 8 : Middleware d'authentification client

Creez un middleware de navigation pour proteger les pages :

// app/middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to) => {
  const { isAuthenticated, fetchUser } = useAuth()
 
  await fetchUser()
 
  if (!isAuthenticated.value) {
    return navigateTo('/login')
  }
})

Etape 9 : Ajouter la validation avec Zod

Installez Zod pour valider les donnees cote serveur :

pnpm add zod

Creez un utilitaire de validation :

// server/utils/validate.ts
import { z } from 'zod'
 
export const createTaskSchema = z.object({
  title: z.string().min(1, 'Le titre est requis').max(200),
  description: z.string().max(1000).optional(),
  priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).default('MEDIUM'),
})
 
export const updateTaskSchema = z.object({
  title: z.string().min(1).max(200).optional(),
  description: z.string().max(1000).nullable().optional(),
  status: z.enum(['TODO', 'IN_PROGRESS', 'DONE']).optional(),
  priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).optional(),
})
 
export const loginSchema = z.object({
  email: z.string().email('Email invalide'),
  password: z.string().min(1, 'Mot de passe requis'),
})
 
export const registerSchema = z.object({
  name: z.string().min(2, 'Le nom doit avoir au moins 2 caracteres'),
  email: z.string().email('Email invalide'),
  password: z.string().min(8, 'Le mot de passe doit avoir au moins 8 caracteres'),
})

Ensuite, mettez a jour votre route de creation de tache pour utiliser la validation :

// server/api/tasks/index.post.ts (mis a jour)
import prisma from '~/server/utils/prisma'
import { createTaskSchema } from '~/server/utils/validate'
 
export default defineEventHandler(async (event) => {
  const user = event.context.user
  const body = await readBody(event)
 
  const result = createTaskSchema.safeParse(body)
  if (!result.success) {
    throw createError({
      statusCode: 400,
      statusMessage: result.error.issues[0].message,
    })
  }
 
  const task = await prisma.task.create({
    data: {
      ...result.data,
      description: result.data.description || null,
      userId: user.id,
    },
  })
 
  return task
})

Etape 10 : Tester votre application

Lancez le serveur de developpement :

pnpm dev

Testez le flux complet :

  1. Accedez a http://localhost:3000 — la page d'accueil s'affiche
  2. Inscrivez-vous en cliquant sur "Inscription" et remplissez le formulaire
  3. Creez une tache depuis le tableau de bord
  4. Changez le statut d'une tache avec le selecteur
  5. Filtrez les taches par statut
  6. Supprimez une tache avec le bouton corbeille

Assurez-vous que PostgreSQL est en cours d'execution et que votre DATABASE_URL est correcte avant de lancer l'application. Si vous utilisez un service cloud comme Neon, verifiez que l'adresse IP de votre machine est autorisee.

Etape 11 : Preparer pour la production

Configuration du build

Mettez a jour nuxt.config.ts pour la production :

// nuxt.config.ts (ajouts production)
export default defineNuxtConfig({
  // ... configuration existante
 
  nitro: {
    preset: 'node-server',
    compressPublicAssets: true,
  },
 
  app: {
    head: {
      title: 'TaskFlow - Gestionnaire de taches',
      meta: [
        { name: 'description', content: 'Application de gestion de taches moderne et efficace' },
      ],
    },
  },
})

Build et lancement

# Build de production
pnpm build
 
# Lancer en production
node .output/server/index.mjs

Deploiement avec Docker

Creez un Dockerfile :

FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
 
FROM base AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm dlx prisma generate
RUN pnpm build
 
FROM base AS production
WORKDIR /app
COPY --from=build /app/.output .output
COPY --from=build /app/node_modules/.prisma node_modules/.prisma
COPY --from=build /app/prisma prisma
 
ENV NODE_ENV=production
EXPOSE 3000
 
CMD ["node", ".output/server/index.mjs"]
# Build et lancement
docker build -t taskflow .
docker run -p 3000:3000 --env-file .env taskflow

Depannage

Erreur "Cannot find module @prisma/client"

Assurez-vous d'avoir execute pnpm dlx prisma generate apres l'installation des dependances.

Les sessions ne persistent pas

Verifiez que SESSION_SECRET est defini dans votre fichier .env. En production, utilisez un secret long et aleatoire.

Erreur de connexion a la base de donnees

Verifiez votre DATABASE_URL et assurez-vous que PostgreSQL est accessible. Pour un service cloud, verifiez les regles de pare-feu.

Les composants Nuxt UI ne s'affichent pas

Assurez-vous que @nuxt/ui est bien dans la liste des modules dans nuxt.config.ts et que les dependances sont installees.

Prochaines etapes

Maintenant que votre application est fonctionnelle, vous pouvez :

  • Ajouter des categories pour organiser les taches par projet
  • Implementer le drag-and-drop avec un tableau Kanban (vue-draggable)
  • Ajouter des notifications par email pour les taches en retard
  • Integrer OAuth avec Google ou GitHub via nuxt-auth-utils
  • Ajouter des tests avec Vitest et Testing Library
  • Mettre en place le SSR selectif pour optimiser les performances

Conclusion

Vous avez construit une application web full-stack complete avec Nuxt 4 et Vue 3. Ce tutoriel vous a permis de decouvrir :

  • La nouvelle structure de projet de Nuxt 4 avec le dossier app/
  • Le systeme de routage automatique base sur les fichiers
  • Les API routes avec le moteur Nitro
  • La gestion de la base de donnees avec Prisma ORM
  • La Composition API de Vue 3 pour les composants reactifs
  • La validation des donnees avec Zod
  • Le deploiement avec Docker

Nuxt 4 offre une experience de developpement full-stack remarquable grace a son integration server-side transparente et son ecosysteme riche de modules. Que vous construisiez un MVP ou une application d'entreprise, Nuxt 4 est un choix solide pour vos projets Vue.js.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur 8 Les Bases de Laravel 11 : Vues.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes