18 juillet 2026 · Nuxt · Prisma · Fullstack
Une app fullstack avec Nuxt : server routes Nitro et Prisma
Construire l'API dans le même projet que le front : les routes serveur de Nitro, Prisma pour la base de données, et useFetch côté client. Un seul dépôt, une seule langue.
L'un des atouts les plus sous-estimés de Nuxt, c'est qu'il n'a pas besoin d'un backend séparé pour être fullstack. Le serveur est déjà là, sous le nom de Nitro. Ajoutez Prisma pour parler à une base de données, et vous tenez une application complète dans un seul dépôt, en TypeScript du front jusqu'à la requête SQL. Voici comment les pièces s'emboîtent.
Le dossier server/
Tout le code serveur vit dans server/, à la racine du projet (hors de
app/, car il ne tourne jamais dans le navigateur). La convention la plus
utile est server/api/ : chaque fichier y devient un point d'accès HTTP.
server/api/articles.get.tsrépond àGET /api/articlesserver/api/articles.post.tsrépond àPOST /api/articlesserver/api/articles/[id].get.tsrépond àGET /api/articles/42
Le suffixe (.get, .post) fixe la méthode HTTP, et l'arborescence fixe
l'URL. Comme pour les pages : le disque décrit l'API.
// server/api/hello.get.ts
export default defineEventHandler((event) => {
const { nom } = getQuery(event)
return { message: `Bonjour ${nom ?? 'le monde'}` }
})
defineEventHandler reçoit un event (la requête), et ce que vous
retournez devient la réponse JSON. Pas de res.json(), pas de
sérialisation manuelle. Les utilitaires comme getQuery, readBody ou
getRouterParam sont auto-importés.
Brancher Prisma
Prisma est un ORM TypeScript : vous décrivez vos tables dans un schéma, il génère un client typé et les migrations. Installation et schéma minimal :
npm install -D prisma
npm install @prisma/client
npx prisma init --datasource-provider sqlite
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Article {
id Int @id @default(autoincrement())
titre String
contenu String
publie Boolean @default(false)
cree_le DateTime @default(now())
}
On applique le schéma à la base et on génère le client :
npx prisma migrate dev --name init
Le client Prisma en singleton
Piège classique : instancier new PrismaClient() dans chaque route ouvre
une nouvelle connexion à chaque appel et épuise la base. On l'instancie une
seule fois, dans un utilitaire serveur réutilisable :
// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()
Placé dans server/utils/, il est auto-importé dans toutes les routes
serveur. Une instance, une réserve de connexions, partagée proprement.
Une vraie route qui lit la base
// server/api/articles.get.ts
export default defineEventHandler(async () => {
return await prisma.article.findMany({
where: { publie: true },
orderBy: { cree_le: 'desc' }
})
})
Et une route qui écrit, avec validation de l'entrée :
// server/api/articles.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
if (!body?.titre || !body?.contenu) {
throw createError({ statusCode: 400, message: 'Titre et contenu requis' })
}
return await prisma.article.create({
data: { titre: body.titre, contenu: body.contenu }
})
})
createError renvoie une vraie erreur HTTP, que le client saura interpréter
plutôt que de recevoir un 200 avec un objet d'erreur déguisé.
Le front consomme sa propre API
Côté page, on ne fait pas d'appel réseau exotique : la même application
appelle sa route avec useFetch. L'URL est relative, il n'y a pas de
problème de CORS, et le rendu serveur pré-remplit déjà les données.
<script setup lang="ts">
const { data: articles, refresh } = await useFetch('/api/articles')
async function publier(nouvel: { titre: string; contenu: string }) {
await $fetch('/api/articles', { method: 'POST', body: nouvel })
await refresh()
}
</script>
<template>
<article v-for="a in articles" :key="a.id">
<h2>{{ a.titre }}</h2>
<p>{{ a.contenu }}</p>
</article>
</template>
useFetch pour lire au rendu (avec cache et hydratation), $fetch pour les
actions ponctuelles comme un POST. La boucle est complète : la page lit une
route Nitro, qui interroge Prisma, qui parle à la base, et tout est typé du
même bout à l'autre.
Ce que ça change au déploiement
Attention : dès qu'il y a des routes serveur et une base, on quitte le monde purement statique. Il faut une cible capable d'exécuter du Node (un VPS, une plateforme applicative, une fonction serverless), et non plus un simple hébergement de fichiers. Nitro sait produire la bonne sortie pour chacune de ces cibles, mais le choix d'hébergement, lui, n'est plus le même que pour un site statique. C'est le prix, assumé, du fullstack.
La leçon
Nuxt fullstack, ce n'est pas « Nuxt plus un backend » : c'est un seul
projet, un seul langage, une seule chaîne de types, du composant Vue jusqu'à
la table SQL. Nitro fournit le serveur, Prisma la base, et useFetch relie
les deux sans couture. Pour un produit mené en petite équipe, cette unité
est un avantage décisif : moins de code de plomberie, moins de contrats
d'API à maintenir, et le remboursement se fait sur chaque fonctionnalité.