Introduction

Pendant 15 ans, mon CV a été un fichier HTML statique. Un seul fichier, hébergé gratuitement, qui faisait le job. Puis un jour, j'ai voulu ajouter un blog. Puis des projets. Puis des commentaires. Puis du multilingue. Et j'ai réalisé que mon fichier HTML ne suffisait plus.

Plutôt que d'empiler du JavaScript vanilla et des appels fetch bricolés, j'ai décidé de tout reconstruire avec une stack moderne. Le résultat : chetana.dev — un portfolio dynamique construit avec Nuxt 4, Neon PostgreSQL et Drizzle ORM, déployé sur Vercel.

Cet article est un retour d'expérience complet : les choix techniques, les pièges rencontrés, et ce que j'ai appris en tant que développeur backend Java qui découvre l'écosystème JavaScript moderne.


Chapitre 1 : Pourquoi migrer ?

Les limites du HTML statique

Mon ancien CV fonctionnait bien pour ce qu'il était :

  • Un fichier index.html de 300 lignes

  • Du CSS inline

  • Hébergé sur GitHub Pages

  • Aucune dépendance, aucun build, aucun serveur

Mais dès que j'ai voulu aller plus loin, les limites sont apparues :

  • Pas de blog : ajouter des articles signifie créer des fichiers HTML manuellement
  • Pas de données dynamiques : chaque modification nécessite un commit + push
  • Pas de commentaires : impossible sans backend
  • Pas de multilingue propre : dupliquer le HTML pour chaque langue ? Non merci
  • Pas de SEO avancé : pas de sitemap dynamique, pas de JSON-LD, pas d'OG tags par page

Le déclic

Le déclic est venu quand j'ai voulu montrer mes compétences en tant qu'Engineering Manager. Un CV statique montre que je sais coder du HTML. Un portfolio dynamique montre que je sais concevoir, architecturer et déployer une application complète.

Chapitre 2 : Le choix de la stack

Pourquoi Nuxt 4 (et pas Next.js) ?

En tant que développeur Java, React et Next.js semblaient le choix évident (plus populaire, plus d'offres d'emploi). Mais j'ai choisi Nuxt/Vue pour plusieurs raisons :

1. La courbe d'apprentissage
Vue est plus accessible que React pour un développeur backend. Le template HTML + script + style dans un fichier .vue ressemble à ce qu'on connaît. Pas de JSX, pas de hooks complexes, pas de "mental model" à repenser.

2. Le système de fichiers comme routeur
Nuxt 4 utilise le file-based routing : un fichier pages/blog/[slug].vue crée automatiquement la route /blog/:slug. Pour un développeur habitué aux routes Spring Boot (@GetMapping), c'est intuitif.

3. Les server routes intégrées
Nuxt inclut Nitro, un serveur HTTP qui permet de créer des API REST directement dans le projet. Un fichier server/api/blog/index.get.ts crée un endpoint GET /api/blog. Pas besoin d'un backend séparé.

4. Le SSR natif
Le Server-Side Rendering est crucial pour le SEO. Nuxt le fait nativement, sans configuration. Chaque page est rendue côté serveur au premier chargement, puis hydratée côté client.

Pourquoi Neon PostgreSQL ?

J'ai envisagé plusieurs options pour la base de données :

OptionAvantageInconvénient
SQLite fichierSimple, gratuitPas de cloud, pas de serverless
SupabaseUI admin, auth intégréeOverhead pour un portfolio
PlanetScaleMySQL serverlessMySQL, pas PostgreSQL
NeonPostgreSQL serverless, gratuitMoins connu

J'ai choisi Neon pour :

  • PostgreSQL : je connais PostgreSQL depuis 10 ans (DJUST, Galeries Lafayette, INFOTEL)

  • Serverless : le compute s'allume uniquement quand il y a une requête. Coût : 0€

  • Free tier généreux : 512 Mo de stockage, 191h de compute/mois

  • Compatible Drizzle : driver natif @neondatabase/serverless

Pourquoi Drizzle ORM ?

Venant de Java/Hibernate, j'avais besoin d'un ORM. Les options en TypeScript :

ORMStyleType-safety
PrismaSchema-first, migration autoBon, mais génère du code
TypeORMDecorators (style Hibernate)Moyen
DrizzleSQL-like, schema-in-codeExcellent

Drizzle m'a convaincu parce que :

  • Le schéma est du code TypeScript : pas de fichier schema séparé, pas de génération de code

  • Les requêtes ressemblent à du SQL : db.select().from(blogPosts).where(eq(...)) — un développeur SQL lit ça sans problème

  • Type-safety de bout en bout : le résultat d'une requête est typé automatiquement

  • Léger : pas de runtime heavy comme Prisma

Chapitre 3 : L'architecture

Structure du projet

chetana-dev/
├── app/
│   ├── pages/           # Routes (file-based routing)
│   │   ├── index.vue    # Page d'accueil
│   │   ├── blog/
│   │   │   ├── index.vue    # Liste des articles
│   │   │   └── [slug].vue   # Article individuel
│   │   ├── projects/
│   │   ├── cv.vue
│   │   └── contact.vue
│   ├── components/      # Composants réutilisables
│   │   ├── BlogCard.vue
│   │   ├── ProjectCard.vue
│   │   ├── Timeline.vue
│   │   └── CommentSection.vue
│   └── composables/     # Logique partagée
│       └── useI18n.ts   # Système i18n custom
├── server/
│   ├── api/             # API REST (Nitro)
│   │   ├── blog/
│   │   ├── experiences.get.ts
│   │   ├── skills.get.ts
│   │   └── comments/
│   ├── db/
│   │   ├── schema.ts    # Schéma Drizzle
│   │   └── seed.ts      # Données initiales
│   └── utils/
│       └── db.ts        # Connexion Neon
└── nuxt.config.ts

Le pattern API

Chaque endpoint suit le même pattern :

  1. Importer la connexion DB (server/utils/db.ts)
  2. Utiliser Drizzle pour la requête
  3. Retourner le résultat (Nitro le sérialise en JSON automatiquement)

C'est minimaliste et efficace. Pas de controllers, pas de services, pas de DTOs — juste des fonctions qui retournent des données.

Le système i18n

Plutôt que d'utiliser une librairie i18n (qui entre en conflit avec nuxt-seo-utils), j'ai créé un composable custom useLocale() :

  • Un ref réactif pour la locale courante (fr/en/km)
  • Une fonction t(key) pour les traductions statiques
  • Une fonction localeField(obj, field) pour les données DB (sélectionne titleFr, titleEn ou titleKm selon la locale)
  • Fallback automatique vers le français si une traduction manque

Chapitre 4 : Les pièges rencontrés

Piège 1 : Le SSR et les composables

En Nuxt, les composables (useLocale(), useRoute()) ne fonctionnent que dans le contexte d'un composant Vue. Appeler useLocale() dans un fichier utilitaire classique provoque une erreur côté serveur.

Solution : toujours appeler les composables dans setup() ou dans un computed, jamais dans une fonction importée globalement.

Piège 2 : Les seeds et l'idempotence

Au début, mes scripts de seed faisaient des INSERT sans vérifier si les données existaient. Résultat : après 3 exécutions, j'avais 18 expériences au lieu de 6 et 126 skills au lieu de 42.

Solution : chaque seed commence par un DELETE de toutes les données existantes, puis fait les INSERT. C'est brutal mais fiable. L'ordre des DELETE respecte les foreign keys (comments → blogPosts → experiences → skills → projects).

Piège 3 : Les variables d'environnement en local

Vercel injecte automatiquement DATABASE_URL en production. En local, j'utilise un fichier .env.local. Mais dotenv par défaut ne charge que .env, pas .env.local.

Solution : ajouter explicitement config({ path: '.env.local' }) dans chaque script de seed.

Piège 4 : Le rendu Markdown

Les articles de blog sont stockés en Markdown dans la base de données. Mais Nuxt ne rend pas le Markdown nativement dans le template.

Solution : un computed dans la page blog qui transforme le Markdown en HTML avec des regex : headers, listes, bold, italic, tables, sauts de ligne. C'est pas aussi complet qu'une librairie Markdown, mais ça suffit pour un blog technique.

Piège 5 : Le conflit nuxt-seo-utils / useI18n

J'avais nommé mon composable useI18n — le même nom que le composable de vue-i18n. Le module @nuxtjs/seo importe internement vue-i18n et le conflit faisait crasher le build.

Solution : renommer le composable en useLocale() et l'exporter depuis un fichier nommé useI18n.ts (le nom du fichier ne pose pas problème, c'est le nom de la fonction exportée qui compte).

Chapitre 5 : Le déploiement sur Vercel

Pourquoi Vercel ?

  • Zero config : Vercel détecte Nuxt automatiquement et configure le build
  • Edge network : le site est servi depuis le CDN le plus proche du visiteur
  • Auto-deploy : chaque push sur main déclenche un déploiement
  • Serverless functions : les server routes Nuxt sont déployées comme des serverless functions
  • Gratuit pour un usage personnel

Le workflow de déploiement

  1. git push origin main
  2. Vercel détecte le push (webhook GitHub)
  3. Vercel exécute npm run build (Nuxt build)
  4. Les fichiers statiques vont sur le CDN
  5. Les server routes deviennent des serverless functions
  6. Le site est live en ~45 secondes

La connexion Neon ↔ Vercel

Neon fournit une connection string PostgreSQL. Je la stocke dans Vercel comme variable d'environnement (DATABASE_URL et NUXT_DATABASE_URL).

Quand une serverless function reçoit une requête :

  1. Le driver @neondatabase/serverless établit une connexion HTTP (pas TCP)

  2. La requête SQL est envoyée via HTTP à Neon

  3. Neon réveille le compute (si endormi), exécute la requête, retourne le résultat

  4. Le tout en 50-200ms (premier appel après cold start : ~500ms)

Chapitre 6 : Les résultats

Performance

MétriqueRésultat
Lighthouse Performance95+
First Contentful Paint< 1s
Time to Interactive< 1.5s
Taille du bundle JS~207 KB (gzippé : 77 KB)
Cold start Neon~500ms
Requête DB warm50-200ms

SEO

  • Sitemap dynamique : génère automatiquement les URLs des articles et projets
  • JSON-LD : schema.org Person + BlogPosting sur chaque article
  • OG/Twitter meta : useSeoMeta() sur chaque page
  • Robots : la page CV est en noindex (contenu similaire à LinkedIn)

Coût mensuel

ServiceCoût
Vercel (Hobby)0€
Neon (Free tier)0€
Domaine chetana.dev~12€/an
Total~1€/mois

Chapitre 7 : Ce que j'ai appris

En tant que développeur Java qui découvre le JavaScript moderne

Ce qui m'a surpris positivement :

  • La vitesse de développement : de l'idée au déploiement en quelques heures, pas en quelques jours

  • Le hot reload : modifier un composant et voir le résultat instantanément, sans redémarrer un serveur Spring Boot

  • La simplicité du déploiement : git push et c'est en production. Pas de Jenkins, pas de Kubernetes, pas de Docker

  • Le typage end-to-end : Drizzle + TypeScript = les erreurs de types sont détectées à la compilation

Ce qui m'a manqué :

  • La rigueur de Java : le typage de TypeScript est bon mais moins strict que Java. Les any sont tentants

  • L'écosystème de tests : JUnit + Mockito est plus mature que Vitest/Jest pour les tests complexes

  • La stabilité : l'écosystème JavaScript bouge trop vite. Ce qui est best practice aujourd'hui sera obsolète dans 6 mois

Le meilleur des deux mondes

Ce projet m'a convaincu que Java et JavaScript sont complémentaires, pas concurrents :

  • Java pour le backend lourd : transactions, multi-tenancy, intégrations enterprise, batch processing
  • Nuxt/Vue pour le frontend et les applications légères : portfolios, blogs, dashboards, outils internes

Un développeur qui maîtrise les deux a un avantage considérable sur le marché.


Chetana YIN — Février 2026
Engineering Manager, développeur Java depuis 2008, converti Nuxt depuis 2025.