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 :
- Node.js 18+ — exécutez
node --versionpour vérifier - Connaissances de base en HTML, CSS et JavaScript
- Familiarité avec les bases de Svelte (utile mais pas obligatoire)
- 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 fichiers | La structure des dossiers définit automatiquement les routes |
| SSR + CSR | Rendu hybride serveur et client |
| Form Actions | Traitement des formulaires côté serveur sans JS client |
| Adaptateurs de déploiement | Déployez partout (Vercel, Node, Cloudflare) |
| Performances exceptionnelles | Bundles 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
useStateniuseEffect
Étape 1 : Créer le projet
Commencez par créer un nouveau projet SvelteKit :
npx sv create notes-appLorsque 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 devOuvrez 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
| Fichier | Rôle |
|---|---|
+page.svelte | Composant UI de la page |
+page.server.ts | Logique serveur (chargement de données, Form Actions) |
+page.ts | Chargement de données universel (serveur + client) |
+layout.svelte | Layout partagé englobant les pages |
+layout.server.ts | Données partagées du layout |
+server.ts | Route API (GET, POST, PUT, DELETE) |
+error.svelte | Page 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-sqlite3Cré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 © {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 identiquesUtilisation 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 buildDéploiement sur Vercel
# Pas besoin de changer l'adaptateur — l'adaptateur par défaut fonctionne avec Vercel
npx vercelDé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 devTester les routes
- Page d'accueil :
http://localhost:5173— affiche les statistiques - Notes :
http://localhost:5173/notes— ajouter, modifier et supprimer - 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-sqlite3404 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 à :
- Créer un projet SvelteKit 2 à partir de zéro
- Le routage basé sur les fichiers avec des paramètres dynamiques
- Les Form Actions pour le traitement sécurisé des formulaires côté serveur
- Le chargement de données avec les fonctions
load - Les routes API avec GET, POST, PUT et DELETE
- Le streaming de données pour un chargement progressif
- Les Hooks pour le traitement des requêtes et le middleware
- 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