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

Ce que vous allez construire
Dans ce tutoriel, vous allez construire une application de gestion de tâches full-stack avec SolidStart — le méta-framework officiel de SolidJS. À la fin, vous aurez une application fonctionnelle avec :
- Routage basé sur les fichiers avec des layouts imbriqués
- Fonctions serveur utilisant
"use server"pour la logique backend sécurisée - Chargement réactif des données avec
queryetcreateAsync - Mutations de données avec
actionpour le traitement des formulaires - Stockage SQLite avec
better-sqlite3 - Support TypeScript complet
- Rendu côté serveur (SSR) avec streaming
Temps requis : 60-90 minutes
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ — exécutez
node --versionpour vérifier - Connaissances de base en HTML, CSS et JavaScript
- Familiarité avec les frameworks réactifs (connaître React ou Solid aide)
- Un éditeur de code — VS Code avec l'extension Solid recommandé
Pourquoi SolidStart ?
Qu'est-ce que SolidStart ?
SolidStart est le framework full-stack officiel de SolidJS. Il combine la réactivité fine de Solid avec un runtime serveur puissant.
| Fonctionnalité | Description |
|---|---|
| Réactivité fine | Met à jour uniquement ce qui a changé — pas de diffing de DOM virtuel |
| Routage par fichiers | Les pages sont définies par la structure du système de fichiers |
| Fonctions serveur | Exécuter du code serveur avec la directive "use server" |
| Modes de rendu multiples | SSR, CSR, SSG et streaming intégrés |
| Construit sur Vinxi | Propulsé par Nitro et Vite sous le capot |
| Déployez partout | Presets pour Vercel, Netlify, Cloudflare, AWS et plus |
Comment se compare SolidStart ?
Si vous avez utilisé Next.js, Nuxt ou SvelteKit, SolidStart remplit le même rôle pour SolidJS. La différence clé est le modèle de réactivité de Solid — des signaux et effets au lieu d'un DOM virtuel — offrant des performances d'exécution exceptionnelles.
Étape 1 : Créer un nouveau projet SolidStart
Ouvrez votre terminal et exécutez :
npx create-solid@latest task-managerQuand demandé, sélectionnez les options suivantes :
- Est-ce un projet SolidStart ? → Oui
- Template →
basic - Utiliser TypeScript ? → Oui
Naviguez dans le projet et installez les dépendances :
cd task-manager
npm installLancez le serveur de développement :
npm run devVisitez http://localhost:3000 — vous devriez voir la page d'accueil par défaut de SolidStart.
Étape 2 : Comprendre la structure du projet
Voici ce que SolidStart a généré pour vous :
task-manager/
├── public/ # Fichiers statiques
├── src/
│ ├── routes/ # Routage basé sur les fichiers
│ │ └── index.tsx # Page d'accueil (/)
│ ├── components/ # Composants réutilisables
│ ├── app.tsx # Shell racine de l'application
│ ├── entry-client.tsx # Point d'entrée client
│ └── entry-server.tsx # Point d'entrée serveur
├── app.config.ts # Configuration SolidStart
├── tsconfig.json
└── package.json
Fichiers clés expliqués
src/app.tsx — Le composant racine qui enveloppe toute votre application. Il configure le routeur et définit le shell HTML :
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
export default function App() {
return (
<Router
root={(props) => (
<Suspense>{props.children}</Suspense>
)}
>
<FileRoutes />
</Router>
);
}src/entry-server.tsx — Gère le rendu côté serveur :
import { createHandler, StartServer } from "@solidjs/start/server";
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body>
<div id="app">{children}</div>
{scripts}
</body>
</html>
)}
/>
));Étape 3 : Configurer la base de données
Installez better-sqlite3 pour une base de données simple basée sur des fichiers :
npm install better-sqlite3
npm install -D @types/better-sqlite3Créez l'utilitaire de base de données dans src/lib/db.ts :
import Database from "better-sqlite3";
import { join } from "path";
const db = new Database(join(process.cwd(), "tasks.db"));
// Activer le mode WAL pour de meilleures performances
db.pragma("journal_mode = WAL");
// Créer la table des tâches
db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT DEFAULT '',
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)
`);
export { db };Étape 4 : Créer les fonctions serveur pour les opérations CRUD
Créez src/lib/tasks.ts pour définir toutes les opérations de données côté serveur :
import { action, query, redirect } from "@solidjs/router";
import { db } from "./db";
// Types
export interface Task {
id: number;
title: string;
description: string;
completed: number;
created_at: string;
updated_at: string;
}
// ─── Requêtes ──────────────────────────────────────────
export const getTasks = query(async (filter?: string) => {
"use server";
let sql = "SELECT * FROM tasks";
if (filter === "active") {
sql += " WHERE completed = 0";
} else if (filter === "completed") {
sql += " WHERE completed = 1";
}
sql += " ORDER BY created_at DESC";
return db.prepare(sql).all() as Task[];
}, "tasks");
export const getTask = query(async (id: number) => {
"use server";
return db.prepare("SELECT * FROM tasks WHERE id = ?").get(id) as Task | undefined;
}, "task");
export const getTaskStats = query(async () => {
"use server";
const total = db.prepare("SELECT COUNT(*) as count FROM tasks").get() as { count: number };
const completed = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE completed = 1").get() as { count: number };
return {
total: total.count,
completed: completed.count,
active: total.count - completed.count,
};
}, "taskStats");
// ─── Actions ──────────────────────────────────────────
export const addTask = action(async (formData: FormData) => {
"use server";
const title = formData.get("title")?.toString().trim();
const description = formData.get("description")?.toString().trim() ?? "";
if (!title) {
throw new Error("Le titre est requis");
}
db.prepare("INSERT INTO tasks (title, description) VALUES (?, ?)").run(title, description);
throw redirect("/");
});
export const toggleTask = action(async (formData: FormData) => {
"use server";
const id = Number(formData.get("id"));
db.prepare(
"UPDATE tasks SET completed = CASE WHEN completed = 0 THEN 1 ELSE 0 END, updated_at = datetime('now') WHERE id = ?"
).run(id);
throw redirect("/");
});
export const deleteTask = action(async (formData: FormData) => {
"use server";
const id = Number(formData.get("id"));
db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
throw redirect("/");
});
export const updateTask = action(async (formData: FormData) => {
"use server";
const id = Number(formData.get("id"));
const title = formData.get("title")?.toString().trim();
const description = formData.get("description")?.toString().trim() ?? "";
if (!title) {
throw new Error("Le titre est requis");
}
db.prepare(
"UPDATE tasks SET title = ?, description = ?, updated_at = datetime('now') WHERE id = ?"
).run(title, description, id);
throw redirect(`/tasks/${id}`);
});Concepts clés expliqués
query()— Enveloppe une fonction serveur pour la récupération de données. Les résultats sont mis en cache et peuvent être préchargés lors de la navigationaction()— Enveloppe une fonction serveur pour les mutations. Les actions invalident automatiquement les caches de requêtes associées"use server"— Cette directive indique à SolidStart d'exécuter la fonction exclusivement sur le serveurthrow redirect("/")— Après une mutation, redirige. Dans SolidStart, les redirections depuis les actions utilisentthrow
Étape 5 : Construire le layout racine avec la navigation
Mettez à jour src/app.tsx pour inclure une barre de navigation :
import { A, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
export default function App() {
return (
<Router
root={(props) => (
<div class="app">
<header class="header">
<div class="container">
<A href="/" class="logo">
Gestionnaire de tâches
</A>
<nav>
<A href="/" end>Toutes les tâches</A>
<A href="/tasks/new">Nouvelle tâche</A>
</nav>
</div>
</header>
<main class="container">
<Suspense fallback={<div class="loading">Chargement...</div>}>
{props.children}
</Suspense>
</main>
</div>
)}
>
<FileRoutes />
</Router>
);
}Étape 6 : Construire la page d'accueil — Liste des tâches
Remplacez le contenu de src/routes/index.tsx par la page de liste de tâches :
import { For, Show } from "solid-js";
import { A, useSearchParams } from "@solidjs/router";
import { createAsync, type RouteDefinition } from "@solidjs/router";
import { getTasks, getTaskStats, toggleTask, deleteTask } from "~/lib/tasks";
export const route = {
preload: () => {
getTasks();
getTaskStats();
},
} satisfies RouteDefinition;
export default function Home() {
const [searchParams, setSearchParams] = useSearchParams();
const filter = () => searchParams.filter as string | undefined;
const tasks = createAsync(() => getTasks(filter()));
const stats = createAsync(() => getTaskStats());
return (
<div>
<div class="page-header">
<h1>Mes tâches</h1>
<A href="/tasks/new" class="btn btn-primary">
+ Ajouter une tâche
</A>
</div>
<Show when={stats()}>
{(s) => (
<div class="stats">
<div class="stat">
<span class="stat-value">{s().total}</span>
<span class="stat-label">Total</span>
</div>
<div class="stat">
<span class="stat-value">{s().active}</span>
<span class="stat-label">Actives</span>
</div>
<div class="stat">
<span class="stat-value">{s().completed}</span>
<span class="stat-label">Terminées</span>
</div>
</div>
)}
</Show>
<div class="filters">
<button
class={`filter-btn ${!filter() ? "active" : ""}`}
onClick={() => setSearchParams({ filter: undefined })}
>
Toutes
</button>
<button
class={`filter-btn ${filter() === "active" ? "active" : ""}`}
onClick={() => setSearchParams({ filter: "active" })}
>
Actives
</button>
<button
class={`filter-btn ${filter() === "completed" ? "active" : ""}`}
onClick={() => setSearchParams({ filter: "completed" })}
>
Terminées
</button>
</div>
<Show
when={tasks()?.length}
fallback={
<div class="empty-state">
<p>Aucune tâche pour le moment. Créez votre première tâche !</p>
<A href="/tasks/new" class="btn btn-primary">
Créer une tâche
</A>
</div>
}
>
<ul class="task-list">
<For each={tasks()}>
{(task) => (
<li class={`task-item ${task.completed ? "completed" : ""}`}>
<form action={toggleTask} method="post" class="toggle-form">
<input type="hidden" name="id" value={task.id} />
<button type="submit" class="checkbox" aria-label="Basculer la tâche">
{task.completed ? "✓" : ""}
</button>
</form>
<A href={`/tasks/${task.id}`} class="task-content">
<span class="task-title">{task.title}</span>
<Show when={task.description}>
<span class="task-desc">{task.description}</span>
</Show>
</A>
<form action={deleteTask} method="post">
<input type="hidden" name="id" value={task.id} />
<button type="submit" class="btn btn-danger btn-sm">
Supprimer
</button>
</form>
</li>
)}
</For>
</ul>
</Show>
</div>
);
}Comment fonctionne le chargement des données
route.preload— Quand un utilisateur navigue vers cette page, SolidStart commence à récupérer les données en avancecreateAsync— Crée une ressource réactive qui suspend le rendu jusqu'à ce que les données soient prêtes<For>— Le composant de boucle optimisé de Solid. Il ne re-rend que les éléments individuels qui changent<Show>— Rendu conditionnel qui évite la création inutile de DOM
Étape 7 : Créer la page de nouvelle tâche
Créez src/routes/tasks/new.tsx :
import { A } from "@solidjs/router";
import { addTask } from "~/lib/tasks";
export default function NewTask() {
return (
<div>
<A href="/" class="back-link">
← Retour aux tâches
</A>
<h1>Créer une nouvelle tâche</h1>
<form action={addTask} method="post" class="task-form">
<div class="form-group">
<label for="title">Titre *</label>
<input
type="text"
id="title"
name="title"
placeholder="Que faut-il faire ?"
required
autofocus
/>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
name="description"
placeholder="Ajouter des détails (optionnel)"
rows="4"
/>
</div>
<div class="form-actions">
<A href="/" class="btn btn-danger">
Annuler
</A>
<button type="submit" class="btn btn-primary">
Créer la tâche
</button>
</div>
</form>
</div>
);
}Comment fonctionnent les formulaires dans SolidStart
Notez qu'il n'y a pas de gestionnaire onSubmit ni de useState. Le formulaire utilise une action HTML native pointant vers l'action serveur addTask. Lors de la soumission :
- SolidStart intercepte la soumission du formulaire
- Sérialise le
FormData - L'envoie à la fonction serveur
- La fonction serveur traite les données et redirige
- Tous les caches
queryassociés sont automatiquement invalidés
C'est de l'amélioration progressive — le formulaire fonctionne même sans JavaScript activé.
Étape 8 : Créer la page de détail de la tâche
Créez src/routes/tasks/[id].tsx — les crochets font de id un paramètre de route dynamique :
import { Show } from "solid-js";
import { A, useParams } from "@solidjs/router";
import { createAsync, type RouteDefinition } from "@solidjs/router";
import { getTask, toggleTask, deleteTask, updateTask } from "~/lib/tasks";
import { createSignal } from "solid-js";
export const route = {
preload: ({ params }) => getTask(Number(params.id)),
} satisfies RouteDefinition;
export default function TaskDetail() {
const params = useParams();
const task = createAsync(() => getTask(Number(params.id)));
const [editing, setEditing] = createSignal(false);
return (
<div>
<A href="/" class="back-link">
← Retour aux tâches
</A>
<Show when={task()} fallback={<p>Tâche introuvable.</p>}>
{(t) => (
<div class="task-detail">
<Show
when={!editing()}
fallback={
<form action={updateTask} method="post" class="edit-form">
<input type="hidden" name="id" value={t().id} />
<div class="form-group">
<label for="title">Titre</label>
<input type="text" id="title" name="title" value={t().title} required />
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" rows="4">
{t().description}
</textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-danger" onClick={() => setEditing(false)}>
Annuler
</button>
<button type="submit" class="btn btn-primary">
Enregistrer
</button>
</div>
</form>
}
>
<div class="detail-header">
<h1 class={t().completed ? "completed-title" : ""}>
{t().title}
</h1>
<div class="detail-actions">
<button class="btn btn-primary btn-sm" onClick={() => setEditing(true)}>
Modifier
</button>
<form action={toggleTask} method="post" style="display:inline">
<input type="hidden" name="id" value={t().id} />
<button type="submit" class="btn btn-sm" style={`background: ${t().completed ? "var(--border)" : "var(--success)"}; color: white;`}>
{t().completed ? "Réactiver" : "Terminer"}
</button>
</form>
<form action={deleteTask} method="post" style="display:inline">
<input type="hidden" name="id" value={t().id} />
<button type="submit" class="btn btn-danger btn-sm">
Supprimer
</button>
</form>
</div>
</div>
<Show when={t().description}>
<div class="detail-description">
<h3>Description</h3>
<p>{t().description}</p>
</div>
</Show>
<div class="detail-meta">
<p><strong>Statut :</strong> <span class={`status ${t().completed ? "done" : "active"}`}>{t().completed ? "Terminée" : "Active"}</span></p>
<p><strong>Créée le :</strong> {new Date(t().created_at).toLocaleDateString("fr-FR")}</p>
<p><strong>Dernière mise à jour :</strong> {new Date(t().updated_at).toLocaleDateString("fr-FR")}</p>
</div>
</Show>
</div>
)}
</Show>
</div>
);
}Routes dynamiques expliquées
[id].tsx— Les crochets créent un segment dynamique./tasks/42associeidà"42"useParams()— Accède aux paramètres dynamiques de manière réactivecreateSignal— L'équivalent Solid deuseStatede React, mais avec une réactivité fine
Étape 9 : Ajouter une route API
SolidStart supporte les routes API pour construire des endpoints REST. Créez src/routes/api/tasks.ts :
import type { APIEvent } from "@solidjs/start/server";
import { db } from "~/lib/db";
import type { Task } from "~/lib/tasks";
export async function GET(event: APIEvent) {
const url = new URL(event.request.url);
const filter = url.searchParams.get("filter");
let sql = "SELECT * FROM tasks";
if (filter === "active") sql += " WHERE completed = 0";
else if (filter === "completed") sql += " WHERE completed = 1";
sql += " ORDER BY created_at DESC";
const tasks = db.prepare(sql).all() as Task[];
return new Response(JSON.stringify(tasks), {
headers: { "Content-Type": "application/json" },
});
}
export async function POST(event: APIEvent) {
const body = await event.request.json();
if (!body.title?.trim()) {
return new Response(JSON.stringify({ error: "Le titre est requis" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const result = db
.prepare("INSERT INTO tasks (title, description) VALUES (?, ?)")
.run(body.title.trim(), body.description?.trim() ?? "");
const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(result.lastInsertRowid);
return new Response(JSON.stringify(task), {
status: 201,
headers: { "Content-Type": "application/json" },
});
}Vous pouvez maintenant tester l'API :
# Récupérer toutes les tâches
curl http://localhost:3000/api/tasks
# Créer une tâche via l'API
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Apprendre SolidStart", "description": "Construire un gestionnaire de tâches"}'Étape 10 : Ajouter la gestion des erreurs
Créez une page de limite d'erreur dans src/routes/*404.tsx :
import { A } from "@solidjs/router";
export default function NotFound() {
return (
<div class="not-found">
<h1>404</h1>
<p>La page que vous recherchez n'existe pas.</p>
<A href="/" class="btn btn-primary">
Retour à l'accueil
</A>
</div>
);
}Étape 11 : Configurer et construire pour la production
Mettez à jour app.config.ts :
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
server: {
preset: "node-server",
},
});Construisez et exécutez :
npm run build
node .output/server/index.mjsDéployer sur d'autres plateformes
| Plateforme | Preset |
|---|---|
| Vercel | vercel |
| Netlify | netlify |
| Cloudflare Pages | cloudflare-pages |
| AWS Lambda | aws-lambda |
| Node.js | node-server |
| Statique (SSG) | static |
Tester votre implémentation
Vérifiez que tout fonctionne :
- Créer des tâches — Naviguez vers
/tasks/new, remplissez le titre et la description, soumettez - Voir les tâches — La page d'accueil affiche toutes les tâches avec les statistiques
- Filtrer les tâches — Cliquez "Actives" ou "Terminées" pour filtrer
- Basculer l'état — Cliquez la case à cocher pour marquer les tâches terminées/actives
- Voir les détails — Cliquez le titre d'une tâche pour voir sa page de détail
- Modifier les tâches — Sur la page de détail, cliquez "Modifier"
- Supprimer les tâches — Cliquez "Supprimer" pour retirer une tâche
- Tester l'API — Utilisez
curlpour tester/api/tasks
Dépannage
Problèmes courants
"Cannot find module 'better-sqlite3'"
Assurez-vous de l'avoir installé : npm install better-sqlite3 @types/better-sqlite3
"tasks.db is locked"
Cela peut arriver si plusieurs processus accèdent à la base de données. Le mode WAL que nous avons activé aide, mais assurez-vous qu'un seul serveur de développement tourne.
Erreurs TypeScript avec les paramètres de route
Convertissez toujours les paramètres de route : Number(params.id) — les paramètres sont toujours des chaînes.
SolidStart vs autres frameworks
| Fonctionnalité | SolidStart | Next.js | SvelteKit | Nuxt |
|---|---|---|---|---|
| Réactivité | Signaux fins | DOM virtuel | Compilation | DOM virtuel (Vue) |
| Taille du bundle | ~7 Ko | ~85 Ko | ~15 Ko | ~60 Ko |
| Fonctions serveur | "use server" | Server Actions | Form actions | Répertoire server/ |
| Chargement données | query + createAsync | fetch dans RSC | Fonctions load | useFetch |
Prochaines étapes
Maintenant que vous avez construit une application full-stack avec SolidStart, voici quelques idées pour aller plus loin :
- Ajouter l'authentification — Utilisez Clerk ou Lucia
- Passer à PostgreSQL — Remplacez SQLite par une base de données de production avec Drizzle ORM
- Ajouter des mises à jour en temps réel — Utilisez WebSockets ou Server-Sent Events
- Implémenter le glisser-déposer — Ajoutez la réorganisation des tâches
- Déployer en production — Essayez de déployer sur Vercel ou Cloudflare Pages
Ressources utiles
Conclusion
Vous avez construit un gestionnaire de tâches complet avec SolidStart, couvrant :
- Le routage basé sur les fichiers avec des paramètres dynamiques
- Les fonctions serveur utilisant la directive
"use server"pour un accès sécurisé aux données - Le chargement réactif des données avec
queryetcreateAsync - Les mutations basées sur les formulaires avec
actionet l'amélioration progressive - Les routes API pour des endpoints REST
- Le stockage SQLite pour la persistance
- Le déploiement en production avec des presets configurables
SolidStart apporte les avantages de performance de la réactivité fine de SolidJS au développement full-stack, avec une excellente expérience développeur propulsée par Vite et Vinxi. Sa petite taille de bundle, son exécution rapide et ses API intuitives en font un choix convaincant pour les applications web modernes.
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 SvelteKit 2 : guide pratique complet
Apprenez à construire une application de gestion de notes complète avec SvelteKit 2. Ce tutoriel pratique couvre le routage basé sur les fichiers, le rendu côté serveur, les Form Actions, les routes API et le déploiement sur Vercel.

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.