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

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.
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

Construire une application full-stack avec TanStack Start : le framework React nouvelle génération
Apprenez à créer une application full-stack complète avec TanStack Start, le meta-framework React propulsé par TanStack Router et Vite. Ce tutoriel couvre le routage fichier, les fonctions serveur, le middleware, l'authentification et le déploiement.

Créer une Application Fullstack avec PocketBase et Next.js en 2026
Apprenez à construire une application fullstack complète avec PocketBase comme backend et Next.js comme frontend. Ce tutoriel couvre l'authentification, le CRUD temps réel et le déploiement.

React Router v7 : Construire une application full-stack avec le Framework Mode
Apprenez à créer une application full-stack complète avec React Router v7 en mode framework. Ce tutoriel couvre le SSR, les loaders, les actions, la validation de formulaires et le déploiement.