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

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
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

Créer une application web full-stack avec Deno 2 et le framework Fresh
Apprenez à créer une application de gestion de tâches complète avec Deno 2 et Fresh. Ce tutoriel pratique couvre l'architecture Islands, le rendu côté serveur, les routes API et Deno KV pour la persistance des données.

Construire une application web full-stack avec SolidStart : guide pratique complet
Apprenez à construire une application de gestion de tâches complète avec SolidStart et SolidJS. Ce tutoriel couvre le routage par fichiers, les fonctions serveur, le chargement réactif des données, les actions et le déploiement.

AI SDK 4.0 : Nouvelles Fonctionnalites et Cas d'Utilisation
Decouvrez les nouvelles fonctionnalites et cas d'utilisation d'AI SDK 4.0, incluant le support PDF, l'utilisation de l'ordinateur et plus encore.