Architecture Moderne : Next.js, TypeScript et PostgreSQL
Architecture Moderne : Next.js, TypeScript et PostgreSQL
Introduction
Choisir une architecture technique n'est jamais anodin. Chaque décision a des conséquences à long terme sur la maintenabilité, la performance, et la scalabilité du projet. Dans cet article, je partage les choix architecturaux faits pour ce portfolio et le raisonnement derrière chaque décision - un exercice de réflexion qui s'applique à tout projet moderne.
Le Framework de Décision
Avant de plonger dans les choix spécifiques, voici les critères qui ont guidé mes décisions :
1. Les 3 Piliers de l'Architecture
Performance ⚡
- Temps de chargement < 2s
- Score Lighthouse 100/100
- Build time < 5s
Developer Experience 🛠️
- Type safety complète
- Hot reload < 100ms
- Documentation claire
Maintenabilité 🔧
- Code lisible et testable
- Évolutivité simple
- Faible dette technique
2. Le Contexte du Projet
// Contraintes et objectifs interface ProjectContext { type: 'Portfolio & Blog'; team: 'Solo developer'; timeline: '2 semaines'; traffic: 'Moyen (1-10k visites/mois)'; budget: 'Minimal (hosting gratuit)'; evolution: 'Ajout régulier de contenu'; }
Choix #1 : Next.js 16 App Router
Alternatives Considérées
Option A : Next.js Pages Router
- ✅ Mature et stable
- ✅ Documentation extensive
- ❌ Patterns anciens
- ❌ Moins performant
Option B : Remix
- ✅ Excellent DX
- ✅ Nested routing natif
- ❌ Écosystème plus petit
- ❌ Moins d'exemples
Option C : Astro
- ✅ Performance extrême
- ✅ Partial hydration
- ❌ Interactivité limitée
- ❌ Courbe d'apprentissage
✅ Choix Final : Next.js 16 App Router
Justification
// Ce qui a fait pencher la balance const reasons = { performance: { serverComponents: 'Réduction de 40% du JS côté client', streaming: 'TTFB < 200ms', turbopack: 'Builds 10x plus rapides' }, developer: { typescript: 'Support natif excellent', routing: 'File-based, intuitif', deployment: 'Vercel zero-config' }, ecosystem: { community: '4M+ développeurs', plugins: 'Bibliothèque riche', hiring: 'Compétences répandues' } };
Implémentation
// Structure App Router optimale app/ ├── (marketing)/ // Groupe de routes │ ├── page.tsx // Homepage │ └── layout.tsx // Layout partagé ├── blog/ │ ├── page.tsx // Liste des articles │ └── [slug]/ │ └── page.tsx // Article individuel ├── api/ │ └── contact/ │ └── route.ts // API Route └── layout.tsx // Root layout // Avantages: // - Colocation des routes et composants // - Layouts imbriqués automatiques // - Loading states intégrés // - Error boundaries par route
Choix #2 : TypeScript en Mode Strict
Le Débat TypeScript vs JavaScript
JavaScript avec JSDoc
- ✅ Pas de compilation
- ✅ Adoption graduelle
- ❌ Type safety partielle
- ❌ Refactoring risqué
✅ TypeScript Strict
- ✅ Erreurs détectées au build
- ✅ Refactoring sûr
- ✅ Autocomplete puissant
- ❌ Configuration initiale
Configuration Optimale
// tsconfig.json - Mode strict activé { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "exactOptionalPropertyTypes": true } }
Bénéfices Réels
// Avant TypeScript : bugs en production function getBlogPost(id) { const post = posts.find(p => p.id === id); return post.title; // ❌ Crash si post undefined } // Avec TypeScript : erreurs au build function getBlogPost(id: string): BlogPost | undefined { const post = posts.find(p => p.id === id); return post?.title; // ✅ Gestion explicite } // Type safety bout-en-bout const post = await prisma.blogPost.findUnique({ where: { id } }); // Type inféré automatiquement
Résultat : 0 bugs liés aux types en production sur 6 mois.
Choix #3 : PostgreSQL + Prisma
Comparaison des Options BDD
MongoDB
- ✅ Schemaless flexible
- ✅ Scaling horizontal simple
- ❌ Pas de relations fortes
- ❌ Transactions limitées
MySQL
- ✅ Mature et stable
- ✅ Performance éprouvée
- ❌ JSON support limité
- ❌ Less modern features
✅ PostgreSQL
- ✅ Relationnel moderne
- ✅ JSON natif (JSONB)
- ✅ Full-text search
- ✅ Extensions riches
Pourquoi Prisma ?
// Alternatives d'ORM interface ORMComparison { typeorm: { pros: ['Decorators', 'Active Record'], cons: ['Type safety partielle', 'Magic'] }, drizzle: { pros: ['Performance', 'SQL-like'], cons: ['Jeune', 'Moins d'outils'] }, prisma: { pros: ['Type safety', 'DX', 'Migrations'], cons: ['Bundle size', 'Courbe apprentissage'] } }
Schéma Prisma : Le Cœur de l'Architecture
// prisma/schema.prisma model BlogPost { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Multilingual support avec JSON title Json // { fr: "...", en: "...", kr: "..." } slug Json content Json // Metadata coverImage String category String tags String[] // SEO seoMetaDescription Json seoKeywords String[] // Publishing isPublished Boolean @default(false) publishedAt DateTime? readingTimeMinutes Int viewCount Int @default(0) // Indexes pour performance @@index([isPublished, publishedAt]) @@index([category]) }
Avantages en Production
// Type safety complète de la DB au frontend async function getPublishedPosts() { const posts = await prisma.blogPost.findMany({ where: { isPublished: true }, orderBy: { publishedAt: 'desc' }, select: { id: true, title: true, slug: true, excerpt: true } }); // Type inféré : BlogPost[] return posts; } // Migrations versionnées // npx prisma migrate dev --name add_view_count // ✅ Historique complet des changements // ✅ Rollback facile // ✅ CI/CD intégré
Choix #4 : Architecture des Composants
Server vs Client Components
Règle de décision :
// Flowchart de décision function shouldBeClientComponent(component: Component): boolean { const checks = { needsState: component.uses(['useState', 'useReducer']), needsEffects: component.uses(['useEffect', 'useLayoutEffect']), needsEvents: component.has('onClick|onChange|onSubmit'), needsBrowser: component.uses(['window', 'localStorage', 'navigator']), }; return Object.values(checks).some(Boolean); } // 80% des composants = Server Components // 20% des composants = Client Components
Exemple Concret
// ❌ Tout en Client (ancien pattern) 'use client'; export default function BlogSection() { const [posts, setPosts] = useState([]); useEffect(() => { fetch('/api/blog').then(r => r.json()).then(setPosts); }, []); return <BlogList posts={posts} />; } // ✅ Séparation Server/Client // Server Component (default) export default async function BlogSection() { const posts = await prisma.blogPost.findMany({ where: { isPublished: true } }); return <BlogList posts={posts} />; // Client component } // Résultat : // - Pas de fetch côté client // - Pas d'état de loading // - SEO optimal // - -40KB de JavaScript
Choix #5 : Stratégie de Styling
Options Évaluées
Tailwind CSS ✅
- Utility-first
- Purge automatique
- Design system cohérent
- Bundle: 8KB
CSS Modules
- Scoped styles
- Zero runtime
- Courbe apprentissage
- Verbeux
Styled Components
- CSS-in-JS
- Dynamique
- Runtime overhead
- Bundle +30KB
Configuration Tailwind Optimale
// tailwind.config.js module.exports = { content: [ './app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', ], theme: { extend: { colors: { primary: { 50: '#eff6ff', // ... palette complète 950: '#172554', }, }, animation: { 'fade-in': 'fadeIn 0.5s ease-out', }, }, }, plugins: [ require('@tailwindcss/typography'), require('@tailwindcss/forms'), ], }; // Résultat : // - Classes purifiées : 8KB final // - Design tokens cohérents // - Dark mode intégré // - Responsive par défaut
Architecture de Déploiement
Vercel : Le Choix Évident pour Next.js
// Configuration zero-downtime export const config = { runtime: 'edge', // Déployé sur Edge Network regions: ['iad1'], // US East (proche Europe) }; // Fonctionnalités utilisées: const vercelFeatures = { edgeFunctions: 'Latence < 50ms globalement', imageOptimization: 'WebP/AVIF automatique', analytics: 'Core Web Vitals tracking', preview: 'Deploy preview par PR', rollback: 'One-click rollback' };
CI/CD Pipeline
# .github/workflows/ci.yml name: CI/CD on: [push, pull_request] jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Install run: npm ci - name: Lint run: npm run lint - name: Type Check run: npx tsc --noEmit - name: Test run: npm test - name: Build run: npm run build - name: Lighthouse run: npx lhci autorun deploy: needs: quality if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Deploy to Vercel run: vercel deploy --prod
Trade-offs et Leçons Apprises
Ce qui a Bien Fonctionné ✅
- Next.js App Router : Performance spectaculaire, DX excellent
- TypeScript Strict : 0 bugs de typage en production
- Prisma : Migrations fluides, type safety parfaite
- Tailwind : Développement rapide, maintenance simple
- Vercel : Déploiement sans friction
Ce que je Ferais Différemment 🔄
- State Management : J'aurais pu ajouter Zustand pour l'état global (mais finalement pas nécessaire)
- Testing : Aurait dû configurer Playwright dès le début
- Monitoring : Sentry aurait dû être ajouté jour 1
- Documentation : JSDoc plus systématique sur les fonctions complexes
Compromis Assumés ⚖️
// Exemples de compromis const tradeoffs = { multilingual: { chosen: 'JSON fields in database', alternative: 'Separate tables per language', reason: 'Simplicité > Normalisation pour 3 langues', cost: 'Queries légèrement plus complexes' }, images: { chosen: 'Vercel Image Optimization', alternative: 'Cloudinary CDN', reason: 'Intégration native, coût nul', cost: 'Vendor lock-in partiel' }, auth: { chosen: 'Pas d'auth pour v1', alternative: 'NextAuth.js', reason: 'Pas de besoin immédiat', cost: 'À ajouter plus tard si nécessaire' } };
Évolutivité et Croissance
Prêt pour la Scalabilité
// Architecture actuelle supporte : const scalability = { traffic: '100k visites/mois sans modification', database: 'PostgreSQL scale jusqu'à 10M rows', cdn: 'Edge network global (Vercel)', cost: '0$ jusqu'à 10k visites, puis ~5$/mois' }; // Évolution future possible : const futureGrowth = { phase2: 'Ajouter Redis pour caching', phase3: 'Séparer API en microservices si nécessaire', phase4: 'Message queue pour jobs asynchrones' };
Conclusion : Les Principes Directeurs
Les choix architecturaux réussis reposent sur ces principes :
- Pragmatisme > Perfectionnisme : Choisir ce qui fonctionne, pas ce qui est à la mode
- Mesurer > Supposer : Données réelles > Intuition
- Itérer > Big Bang : Évolution graduelle > Réécriture complète
- Developer Experience = Business Value : DX améliore productivité et qualité
- Contraintes = Clarté : Les limites forcent les bonnes décisions
Métriques de succès de cette architecture :
- Build time : 3.5s (objectif < 5s) ✅
- Lighthouse : 100/100 (objectif 95+) ✅
- Type coverage : 100% (objectif 95%+) ✅
- Bugs en production : 0 sur 6 mois ✅
- Time to deploy : < 2 min ✅
L'architecture n'est jamais "terminée" - c'est un processus continu d'apprentissage, d'adaptation, et d'amélioration. Ce qui compte, c'est de partir sur de bonnes fondations qui permettent d'évoluer sans réécriture complète.
Ressources complémentaires :