Construire une application web full-stack avec SvelteKit 2 : guide pratique complet

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Ce que vous allez construire

Dans ce tutoriel, vous allez construire une application de gestion de notes complète avec SvelteKit 2 — le framework officiel pour créer des applications Svelte. À la fin, vous aurez une application fonctionnelle avec :

  • Routage basé sur les fichiers avec des layouts imbriqués
  • Rendu côté serveur (SSR) pour des performances ultra-rapides
  • Form Actions pour traiter les formulaires sans JavaScript côté client
  • Routes API pour des opérations CRUD complètes
  • Stockage de données avec SQLite via better-sqlite3
  • TypeScript entièrement intégré
  • Design responsive avec du CSS moderne

Temps requis : 60-90 minutes


Prérequis

Avant de commencer, assurez-vous de disposer de :

  1. Node.js 18+ — exécutez node --version pour vérifier
  2. Connaissances de base en HTML, CSS et JavaScript
  3. Familiarité avec les bases de Svelte (utile mais pas obligatoire)
  4. Un éditeur de code (VS Code avec l'extension Svelte recommandé)

Pourquoi SvelteKit 2 ?

Qu'est-ce que SvelteKit ?

SvelteKit est le framework officiel pour construire des applications web avec Svelte. Il combine les meilleures fonctionnalités des frameworks modernes :

FonctionnalitéDescription
Routage fichiersLa structure des dossiers définit automatiquement les routes
SSR + CSRRendu hybride serveur et client
Form ActionsTraitement des formulaires côté serveur sans JS client
Adaptateurs de déploiementDéployez partout (Vercel, Node, Cloudflare)
Performances exceptionnellesBundles plus petits que React/Next.js

Pourquoi Svelte plutôt que React ?

Svelte fonctionne comme un compilateur plutôt qu'une bibliothèque runtime. Cela signifie :

  • Bundles 40-70% plus petits comparé à React
  • Pas de Virtual DOM — mises à jour directes sur le vrai DOM
  • Moins de code — syntaxe plus simple et plus claire
  • Réactivité intégrée — pas de useState ni useEffect

Étape 1 : Créer le projet

Commencez par créer un nouveau projet SvelteKit :

npx sv create notes-app

Lorsque les options apparaissent, choisissez :

┌  Welcome to the Svelte CLI!
│
◇  Which template would you like?
│  SvelteKit minimal
│
◇  Add type checking with TypeScript?
│  Yes, using TypeScript syntax
│
◇  What would you like to add to your project?
│  prettier, eslint
│
◇  Which package manager do you want to install dependencies with?
│  npm
│
└  You're all set!

Entrez dans le répertoire et lancez le serveur de développement :

cd notes-app
npm run dev

Ouvrez http://localhost:5173 — vous verrez la page d'accueil par défaut.


Étape 2 : Comprendre la structure du projet

notes-app/
├── src/
│   ├── lib/           # Bibliothèques et composants partagés
│   │   └── index.ts
│   ├── routes/        # Pages de l'application (routage fichiers)
│   │   └── +page.svelte
│   ├── app.html       # Template HTML principal
│   └── app.d.ts       # Types TypeScript
├── static/            # Fichiers statiques (images, polices)
├── svelte.config.js   # Configuration SvelteKit
├── tsconfig.json
├── vite.config.ts
└── package.json

Fichiers spéciaux dans SvelteKit

FichierRôle
+page.svelteComposant UI de la page
+page.server.tsLogique serveur (chargement de données, Form Actions)
+page.tsChargement de données universel (serveur + client)
+layout.svelteLayout partagé englobant les pages
+layout.server.tsDonnées partagées du layout
+server.tsRoute API (GET, POST, PUT, DELETE)
+error.sveltePage d'erreur personnalisée

Étape 3 : Configurer la base de données

Installez better-sqlite3 pour la gestion des données :

npm install better-sqlite3
npm install -D @types/better-sqlite3

Créez le fichier de configuration de la base de données :

// src/lib/server/db.ts
import Database from 'better-sqlite3';
import { dev } from '$app/environment';
import path from 'path';
 
const dbPath = dev ? 'notes.db' : path.join(process.cwd(), 'notes.db');
const db = new Database(dbPath);
 
// Activer le mode WAL pour de meilleures performances
db.pragma('journal_mode = WAL');
 
// Créer la table des notes
db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT NOT NULL DEFAULT '',
    color TEXT NOT NULL DEFAULT '#ffffff',
    pinned INTEGER NOT NULL DEFAULT 0,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
  )
`);
 
export default db;

Créez le fichier de types :

// src/lib/types.ts
export interface Note {
  id: number;
  title: string;
  content: string;
  color: string;
  pinned: number;
  created_at: string;
  updated_at: string;
}

Étape 4 : Construire le layout principal

Remplacez le contenu du layout principal :

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
 
  let { children }: { children: Snippet } = $props();
</script>
 
<div class="app">
  <header>
    <nav>
      <a href="/" class="logo">📝 Mes Notes</a>
      <div class="nav-links">
        <a href="/">Accueil</a>
        <a href="/notes">Notes</a>
        <a href="/about">À propos</a>
      </div>
    </nav>
  </header>
 
  <main>
    {@render children()}
  </main>
 
  <footer>
    <p>Construit avec SvelteKit 2 &copy; {new Date().getFullYear()}</p>
  </footer>
</div>
 
<style>
  :global(body) {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
    background: #f5f5f5;
    color: #333;
  }
 
  .app {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }
 
  header {
    background: #1a1a2e;
    color: white;
    padding: 1rem 2rem;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  }
 
  nav {
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
 
  .logo {
    font-size: 1.5rem;
    font-weight: bold;
    color: white;
    text-decoration: none;
  }
 
  .nav-links {
    display: flex;
    gap: 1.5rem;
  }
 
  .nav-links a {
    color: #a8a8b3;
    text-decoration: none;
    transition: color 0.2s;
  }
 
  .nav-links a:hover {
    color: white;
  }
 
  main {
    flex: 1;
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
    width: 100%;
    box-sizing: border-box;
  }
 
  footer {
    background: #1a1a2e;
    color: #a8a8b3;
    text-align: center;
    padding: 1rem;
    margin-top: auto;
  }
</style>

Étape 5 : La page d'accueil

<!-- src/routes/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
 
  let { data }: { data: PageData } = $props();
</script>
 
<svelte:head>
  <title>Mes Notes — Application SvelteKit</title>
</svelte:head>
 
<section class="hero">
  <h1>Bienvenue dans votre application de notes</h1>
  <p>Une application full-stack construite avec SvelteKit 2, TypeScript et SQLite</p>
  <a href="/notes" class="cta-button">Commencer →</a>
</section>
 
<section class="stats">
  <div class="stat-card">
    <span class="stat-number">{data.totalNotes}</span>
    <span class="stat-label">Notes</span>
  </div>
  <div class="stat-card">
    <span class="stat-number">{data.pinnedNotes}</span>
    <span class="stat-label">Épinglées</span>
  </div>
  <div class="stat-card">
    <span class="stat-number">{data.recentNotes}</span>
    <span class="stat-label">Cette semaine</span>
  </div>
</section>
 
<style>
  .hero {
    text-align: center;
    padding: 4rem 2rem;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border-radius: 16px;
    color: white;
    margin-bottom: 2rem;
  }
 
  .hero h1 {
    font-size: 2.5rem;
    margin-bottom: 1rem;
  }
 
  .hero p {
    font-size: 1.2rem;
    opacity: 0.9;
    margin-bottom: 2rem;
  }
 
  .cta-button {
    display: inline-block;
    padding: 0.8rem 2rem;
    background: white;
    color: #667eea;
    border-radius: 8px;
    text-decoration: none;
    font-weight: bold;
    font-size: 1.1rem;
    transition: transform 0.2s;
  }
 
  .cta-button:hover {
    transform: translateY(-2px);
  }
 
  .stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1.5rem;
  }
 
  .stat-card {
    background: white;
    padding: 2rem;
    border-radius: 12px;
    text-align: center;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
 
  .stat-number {
    display: block;
    font-size: 3rem;
    font-weight: bold;
    color: #667eea;
  }
 
  .stat-label {
    display: block;
    color: #666;
    margin-top: 0.5rem;
  }
</style>

Créez le fichier de chargement de données :

// src/routes/+page.server.ts
import db from '$lib/server/db';
 
export function load() {
  const totalNotes = db.prepare('SELECT COUNT(*) as count FROM notes').get() as { count: number };
  const pinnedNotes = db.prepare('SELECT COUNT(*) as count FROM notes WHERE pinned = 1').get() as { count: number };
  const recentNotes = db.prepare(
    "SELECT COUNT(*) as count FROM notes WHERE created_at >= datetime('now', '-7 days')"
  ).get() as { count: number };
 
  return {
    totalNotes: totalNotes.count,
    pinnedNotes: pinnedNotes.count,
    recentNotes: recentNotes.count
  };
}

Étape 6 : La page de liste des notes

Voici la page principale — affichage et ajout de notes avec les Form Actions :

// src/routes/notes/+page.server.ts
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
 
export function load() {
  const notes = db.prepare(
    'SELECT * FROM notes ORDER BY pinned DESC, updated_at DESC'
  ).all() as Note[];
 
  return { notes };
}
 
export const actions: Actions = {
  create: async ({ request }) => {
    const formData = await request.formData();
    const title = formData.get('title')?.toString().trim();
    const content = formData.get('content')?.toString().trim() ?? '';
    const color = formData.get('color')?.toString() ?? '#ffffff';
 
    if (!title) {
      return fail(400, { error: 'Le titre est obligatoire', title, content, color });
    }
 
    if (title.length > 200) {
      return fail(400, { error: 'Le titre est trop long (200 caractères maximum)', title, content, color });
    }
 
    db.prepare(
      'INSERT INTO notes (title, content, color) VALUES (?, ?, ?)'
    ).run(title, content, color);
 
    return { success: true };
  },
 
  delete: async ({ request }) => {
    const formData = await request.formData();
    const id = formData.get('id');
 
    if (!id) {
      return fail(400, { error: "L'identifiant de la note est requis" });
    }
 
    db.prepare('DELETE FROM notes WHERE id = ?').run(id);
    return { success: true };
  },
 
  togglePin: async ({ request }) => {
    const formData = await request.formData();
    const id = formData.get('id');
 
    if (!id) {
      return fail(400, { error: "L'identifiant de la note est requis" });
    }
 
    db.prepare(
      'UPDATE notes SET pinned = CASE WHEN pinned = 1 THEN 0 ELSE 1 END, updated_at = datetime(\'now\') WHERE id = ?'
    ).run(id);
 
    return { success: true };
  }
};
<!-- src/routes/notes/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { PageData, ActionData } from './$types';
 
  let { data, form }: { data: PageData; form: ActionData } = $props();
 
  let showForm = $state(false);
 
  const colors = [
    { value: '#ffffff', label: 'Blanc' },
    { value: '#fff3cd', label: 'Jaune' },
    { value: '#d1ecf1', label: 'Bleu' },
    { value: '#d4edda', label: 'Vert' },
    { value: '#f8d7da', label: 'Rose' },
    { value: '#e2d9f3', label: 'Violet' }
  ];
</script>
 
<svelte:head>
  <title>Notes — Application SvelteKit</title>
</svelte:head>
 
<div class="notes-page">
  <div class="page-header">
    <h1>Mes Notes</h1>
    <button class="add-btn" onclick={() => showForm = !showForm}>
      {showForm ? '✕ Annuler' : '+ Nouvelle note'}
    </button>
  </div>
 
  {#if showForm}
    <form method="POST" action="?/create" use:enhance class="note-form">
      {#if form?.error}
        <div class="error">{form.error}</div>
      {/if}
 
      <input
        type="text"
        name="title"
        placeholder="Titre de la note..."
        value={form?.title ?? ''}
        required
        maxlength="200"
      />
 
      <textarea
        name="content"
        placeholder="Contenu de la note..."
        rows="4"
      >{form?.content ?? ''}</textarea>
 
      <div class="color-picker">
        <span>Couleur :</span>
        {#each colors as color}
          <label class="color-option">
            <input
              type="radio"
              name="color"
              value={color.value}
              checked={color.value === (form?.color ?? '#ffffff')}
            />
            <span
              class="color-swatch"
              style="background: {color.value}"
              title={color.label}
            ></span>
          </label>
        {/each}
      </div>
 
      <button type="submit" class="submit-btn">Enregistrer la note</button>
    </form>
  {/if}
 
  {#if data.notes.length === 0}
    <div class="empty-state">
      <p>Aucune note pour le moment. Ajoutez votre première note !</p>
    </div>
  {:else}
    <div class="notes-grid">
      {#each data.notes as note (note.id)}
        <div class="note-card" style="background: {note.color}">
          <div class="note-header">
            <h3>{note.title}</h3>
            <div class="note-actions">
              <form method="POST" action="?/togglePin" use:enhance>
                <input type="hidden" name="id" value={note.id} />
                <button type="submit" class="icon-btn" title={note.pinned ? 'Désépingler' : 'Épingler'}>
                  {note.pinned ? '📌' : '📍'}
                </button>
              </form>
              <form method="POST" action="?/delete" use:enhance>
                <input type="hidden" name="id" value={note.id} />
                <button type="submit" class="icon-btn delete" title="Supprimer">🗑️</button>
              </form>
            </div>
          </div>
 
          {#if note.content}
            <p class="note-content">{note.content}</p>
          {/if}
 
          <time class="note-date">
            {new Date(note.updated_at).toLocaleDateString('fr-FR', {
              year: 'numeric',
              month: 'short',
              day: 'numeric'
            })}
          </time>
        </div>
      {/each}
    </div>
  {/if}
</div>
 
<style>
  .notes-page {
    max-width: 900px;
    margin: 0 auto;
  }
 
  .page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 2rem;
  }
 
  .page-header h1 {
    font-size: 2rem;
    color: #1a1a2e;
  }
 
  .add-btn {
    padding: 0.6rem 1.2rem;
    background: #667eea;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 1rem;
    transition: background 0.2s;
  }
 
  .add-btn:hover {
    background: #5a6fd6;
  }
 
  .note-form {
    background: white;
    padding: 1.5rem;
    border-radius: 12px;
    margin-bottom: 2rem;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }
 
  .error {
    background: #f8d7da;
    color: #721c24;
    padding: 0.8rem;
    border-radius: 6px;
    font-size: 0.9rem;
  }
 
  input[type="text"],
  textarea {
    padding: 0.8rem;
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    font-size: 1rem;
    transition: border-color 0.2s;
    font-family: inherit;
  }
 
  input[type="text"]:focus,
  textarea:focus {
    outline: none;
    border-color: #667eea;
  }
 
  .color-picker {
    display: flex;
    align-items: center;
    gap: 0.8rem;
  }
 
  .color-option {
    cursor: pointer;
  }
 
  .color-option input {
    display: none;
  }
 
  .color-swatch {
    display: inline-block;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 2px solid #ddd;
    transition: transform 0.2s;
  }
 
  .color-option input:checked + .color-swatch {
    border-color: #667eea;
    transform: scale(1.2);
  }
 
  .submit-btn {
    padding: 0.8rem;
    background: #667eea;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 1rem;
    font-weight: bold;
  }
 
  .submit-btn:hover {
    background: #5a6fd6;
  }
 
  .empty-state {
    text-align: center;
    padding: 4rem 2rem;
    color: #999;
    font-size: 1.1rem;
  }
 
  .notes-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 1.5rem;
  }
 
  .note-card {
    padding: 1.5rem;
    border-radius: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    transition: transform 0.2s, box-shadow 0.2s;
  }
 
  .note-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  }
 
  .note-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 0.8rem;
  }
 
  .note-header h3 {
    margin: 0;
    font-size: 1.1rem;
    color: #1a1a2e;
    flex: 1;
  }
 
  .note-actions {
    display: flex;
    gap: 0.3rem;
  }
 
  .icon-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 1rem;
    padding: 0.2rem;
    opacity: 0.6;
    transition: opacity 0.2s;
  }
 
  .icon-btn:hover {
    opacity: 1;
  }
 
  .note-content {
    color: #555;
    font-size: 0.95rem;
    line-height: 1.5;
    margin-bottom: 1rem;
  }
 
  .note-date {
    font-size: 0.8rem;
    color: #999;
  }
</style>

Étape 7 : La page de modification

Créez la route de modification avec un paramètre dynamique :

// src/routes/notes/[id]/+page.server.ts
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { error, fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
 
export const load: PageServerLoad = async ({ params }) => {
  const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(params.id) as Note | undefined;
 
  if (!note) {
    throw error(404, 'Note introuvable');
  }
 
  return { note };
};
 
export const actions: Actions = {
  update: async ({ request, params }) => {
    const formData = await request.formData();
    const title = formData.get('title')?.toString().trim();
    const content = formData.get('content')?.toString().trim() ?? '';
    const color = formData.get('color')?.toString() ?? '#ffffff';
 
    if (!title) {
      return fail(400, { error: 'Le titre est obligatoire' });
    }
 
    db.prepare(
      "UPDATE notes SET title = ?, content = ?, color = ?, updated_at = datetime('now') WHERE id = ?"
    ).run(title, content, color, params.id);
 
    redirect(303, '/notes');
  }
};
<!-- src/routes/notes/[id]/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { PageData, ActionData } from './$types';
 
  let { data, form }: { data: PageData; form: ActionData } = $props();
 
  const colors = [
    { value: '#ffffff', label: 'Blanc' },
    { value: '#fff3cd', label: 'Jaune' },
    { value: '#d1ecf1', label: 'Bleu' },
    { value: '#d4edda', label: 'Vert' },
    { value: '#f8d7da', label: 'Rose' },
    { value: '#e2d9f3', label: 'Violet' }
  ];
</script>
 
<svelte:head>
  <title>Modifier : {data.note.title}</title>
</svelte:head>
 
<div class="edit-page">
  <h1>Modifier la note</h1>
 
  <form method="POST" action="?/update" use:enhance class="edit-form">
    {#if form?.error}
      <div class="error">{form.error}</div>
    {/if}
 
    <label>
      Titre
      <input type="text" name="title" value={data.note.title} required maxlength="200" />
    </label>
 
    <label>
      Contenu
      <textarea name="content" rows="8">{data.note.content}</textarea>
    </label>
 
    <div class="color-picker">
      <span>Couleur :</span>
      {#each colors as color}
        <label class="color-option">
          <input
            type="radio"
            name="color"
            value={color.value}
            checked={color.value === data.note.color}
          />
          <span class="color-swatch" style="background: {color.value}" title={color.label}></span>
        </label>
      {/each}
    </div>
 
    <div class="form-actions">
      <a href="/notes" class="cancel-btn">Annuler</a>
      <button type="submit" class="save-btn">Enregistrer les modifications</button>
    </div>
  </form>
</div>
 
<style>
  .edit-page {
    max-width: 700px;
    margin: 0 auto;
  }
 
  .edit-page h1 {
    color: #1a1a2e;
    margin-bottom: 1.5rem;
  }
 
  .edit-form {
    background: white;
    padding: 2rem;
    border-radius: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    display: flex;
    flex-direction: column;
    gap: 1.2rem;
  }
 
  label {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    font-weight: 500;
    color: #333;
  }
 
  input[type="text"],
  textarea {
    padding: 0.8rem;
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    font-size: 1rem;
    font-family: inherit;
  }
 
  input[type="text"]:focus,
  textarea:focus {
    outline: none;
    border-color: #667eea;
  }
 
  .error {
    background: #f8d7da;
    color: #721c24;
    padding: 0.8rem;
    border-radius: 6px;
  }
 
  .color-picker {
    display: flex;
    align-items: center;
    gap: 0.8rem;
  }
 
  .color-option input { display: none; }
 
  .color-swatch {
    display: inline-block;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 2px solid #ddd;
    cursor: pointer;
    transition: transform 0.2s;
  }
 
  .color-option input:checked + .color-swatch {
    border-color: #667eea;
    transform: scale(1.2);
  }
 
  .form-actions {
    display: flex;
    justify-content: flex-end;
    gap: 1rem;
    margin-top: 1rem;
  }
 
  .cancel-btn {
    padding: 0.8rem 1.5rem;
    color: #666;
    text-decoration: none;
    border-radius: 8px;
    border: 2px solid #e0e0e0;
  }
 
  .save-btn {
    padding: 0.8rem 1.5rem;
    background: #667eea;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-weight: bold;
    font-size: 1rem;
  }
 
  .save-btn:hover {
    background: #5a6fd6;
  }
</style>

Étape 8 : Routes API

Créez des routes API RESTful pour accéder aux notes programmatiquement :

// src/routes/api/notes/+server.ts
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { json } from '@sveltejs/kit';
 
export function GET({ url }) {
  const search = url.searchParams.get('search');
  const pinned = url.searchParams.get('pinned');
 
  let query = 'SELECT * FROM notes';
  const conditions: string[] = [];
  const params: unknown[] = [];
 
  if (search) {
    conditions.push('(title LIKE ? OR content LIKE ?)');
    params.push(`%${search}%`, `%${search}%`);
  }
 
  if (pinned === 'true') {
    conditions.push('pinned = 1');
  }
 
  if (conditions.length > 0) {
    query += ' WHERE ' + conditions.join(' AND ');
  }
 
  query += ' ORDER BY pinned DESC, updated_at DESC';
 
  const notes = db.prepare(query).all(...params) as Note[];
  return json(notes);
}
 
export async function POST({ request }) {
  const { title, content, color } = await request.json();
 
  if (!title?.trim()) {
    return json({ error: 'Le titre est obligatoire' }, { status: 400 });
  }
 
  const result = db.prepare(
    'INSERT INTO notes (title, content, color) VALUES (?, ?, ?)'
  ).run(title.trim(), content?.trim() ?? '', color ?? '#ffffff');
 
  const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(result.lastInsertRowid) as Note;
  return json(note, { status: 201 });
}
// src/routes/api/notes/[id]/+server.ts
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { json, error } from '@sveltejs/kit';
 
export function GET({ params }) {
  const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(params.id) as Note | undefined;
 
  if (!note) {
    throw error(404, 'Note introuvable');
  }
 
  return json(note);
}
 
export async function PUT({ params, request }) {
  const { title, content, color } = await request.json();
 
  if (!title?.trim()) {
    return json({ error: 'Le titre est obligatoire' }, { status: 400 });
  }
 
  const existing = db.prepare('SELECT id FROM notes WHERE id = ?').get(params.id);
  if (!existing) {
    throw error(404, 'Note introuvable');
  }
 
  db.prepare(
    "UPDATE notes SET title = ?, content = ?, color = ?, updated_at = datetime('now') WHERE id = ?"
  ).run(title.trim(), content?.trim() ?? '', color ?? '#ffffff', params.id);
 
  const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(params.id) as Note;
  return json(note);
}
 
export function DELETE({ params }) {
  const existing = db.prepare('SELECT id FROM notes WHERE id = ?').get(params.id);
  if (!existing) {
    throw error(404, 'Note introuvable');
  }
 
  db.prepare('DELETE FROM notes WHERE id = ?').run(params.id);
  return json({ success: true });
}

Étape 9 : Page d'erreur personnalisée

<!-- src/routes/+error.svelte -->
<script lang="ts">
  import { page } from '$app/state';
</script>
 
<div class="error-page">
  <h1>{page.status}</h1>
  <p>{page.error?.message ?? 'Une erreur inattendue est survenue'}</p>
  <a href="/">Retour à l'accueil</a>
</div>
 
<style>
  .error-page {
    text-align: center;
    padding: 4rem 2rem;
  }
 
  .error-page h1 {
    font-size: 6rem;
    color: #667eea;
    margin-bottom: 0.5rem;
  }
 
  .error-page p {
    font-size: 1.3rem;
    color: #666;
    margin-bottom: 2rem;
  }
 
  .error-page a {
    padding: 0.8rem 2rem;
    background: #667eea;
    color: white;
    border-radius: 8px;
    text-decoration: none;
  }
</style>

Étape 10 : Hooks et Middleware

SvelteKit fournit un système de hooks puissant pour le traitement des requêtes :

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
 
export const handle: Handle = async ({ event, resolve }) => {
  // Mesurer le temps de réponse
  const start = Date.now();
 
  const response = await resolve(event);
 
  const duration = Date.now() - start;
  console.log(`${event.request.method} ${event.url.pathname} — ${duration}ms`);
 
  // Ajouter des en-têtes de sécurité
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
 
  return response;
};

Étape 11 : Chargement progressif avec le Streaming

Une fonctionnalité avancée de SvelteKit — vous pouvez streamer les données lentes progressivement :

// src/routes/notes/+page.server.ts (version améliorée)
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
 
export function load() {
  // Les données rapides se chargent immédiatement
  const notes = db.prepare(
    'SELECT * FROM notes ORDER BY pinned DESC, updated_at DESC'
  ).all() as Note[];
 
  // Les données lentes sont streamées plus tard (ex : statistiques complexes)
  const stats = new Promise<{ wordCount: number }>((resolve) => {
    const result = db.prepare(
      "SELECT SUM(LENGTH(content) - LENGTH(REPLACE(content, ' ', '')) + 1) as wordCount FROM notes"
    ).get() as { wordCount: number | null };
    resolve({ wordCount: result.wordCount ?? 0 });
  });
 
  return {
    notes,
    streamed: { stats }
  };
}
 
// ... les actions restent identiques

Utilisation des données streamées dans l'interface :

{#await data.streamed.stats}
  <p class="loading">Calcul des statistiques en cours...</p>
{:then stats}
  <p class="word-count">Total de mots : {stats.wordCount}</p>
{/await}

Étape 12 : Configuration du déploiement

Déploiement sur Node.js

npm install @sveltejs/adapter-node
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
 
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter({
      out: 'build',
      precompress: true
    })
  }
};
 
export default config;
# Construire et déployer
npm run build
node build

Déploiement sur Vercel

# Pas besoin de changer l'adaptateur — l'adaptateur par défaut fonctionne avec Vercel
npx vercel

Déploiement avec Docker

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/build ./build
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "build"]

Tester votre application

Exécuter localement

npm run dev

Tester les routes

  1. Page d'accueil : http://localhost:5173 — affiche les statistiques
  2. Notes : http://localhost:5173/notes — ajouter, modifier et supprimer
  3. API : curl http://localhost:5173/api/notes — retourne du JSON

Tester les Form Actions

Les Form Actions fonctionnent sans JavaScript ! Essayez de désactiver le JS dans votre navigateur — les formulaires continueront à fonctionner car ils sont traités côté serveur.


Dépannage

Erreur : Cannot find module '$lib/server/db'

Assurez-vous d'avoir créé le répertoire src/lib/server/ et que le fichier db.ts existe dedans.

Erreur : better-sqlite3 ne fonctionne pas

npm rebuild better-sqlite3

404 sur les routes dynamiques

Assurez-vous que le nom du dossier contient des crochets : [id] et non id.

Les données ne se mettent pas à jour sur la page

Vérifiez que use:enhance est ajouté à l'élément <form> — sans cela, la page entière sera rechargée.


Ce que vous avez appris

Dans ce tutoriel, vous avez appris à :

  1. Créer un projet SvelteKit 2 à partir de zéro
  2. Le routage basé sur les fichiers avec des paramètres dynamiques
  3. Les Form Actions pour le traitement sécurisé des formulaires côté serveur
  4. Le chargement de données avec les fonctions load
  5. Les routes API avec GET, POST, PUT et DELETE
  6. Le streaming de données pour un chargement progressif
  7. Les Hooks pour le traitement des requêtes et le middleware
  8. Le déploiement sur Node.js, Vercel et Docker

Prochaines étapes

  • Authentification : Ajoutez la connexion avec Lucia Auth
  • Base de données de production : Remplacez SQLite par PostgreSQL via Drizzle ORM
  • Tests : Ajoutez des tests E2E avec Playwright
  • PWA : Convertissez l'application en Progressive Web App
  • Documentation officielle : svelte.dev/docs/kit

Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Adaptateur MCP WordPress : Rendez votre site compatible avec les agents IA.

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