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-appQuand 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 devVotre 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/fontsCreez 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 initDefinissez 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 initCreez un utilitaire serveur pour acceder au client Prisma :
// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prismaEtape 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/bcryptjsEtape 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 zodCreez 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 devTestez le flux complet :
- Accedez a
http://localhost:3000— la page d'accueil s'affiche - Inscrivez-vous en cliquant sur "Inscription" et remplissez le formulaire
- Creez une tache depuis le tableau de bord
- Changez le statut d'une tache avec le selecteur
- Filtrez les taches par statut
- 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.mjsDeploiement 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 taskflowDepannage
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.