Construire une application web full-stack avec SolidStart : 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 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 query et createAsync
  • Mutations de données avec action pour 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 :

  1. Node.js 20+ — exécutez node --version pour vérifier
  2. Connaissances de base en HTML, CSS et JavaScript
  3. Familiarité avec les frameworks réactifs (connaître React ou Solid aide)
  4. 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é fineMet à jour uniquement ce qui a changé — pas de diffing de DOM virtuel
Routage par fichiersLes pages sont définies par la structure du système de fichiers
Fonctions serveurExécuter du code serveur avec la directive "use server"
Modes de rendu multiplesSSR, CSR, SSG et streaming intégrés
Construit sur VinxiPropulsé par Nitro et Vite sous le capot
Déployez partoutPresets 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-manager

Quand demandé, sélectionnez les options suivantes :

  • Est-ce un projet SolidStart ? → Oui
  • Templatebasic
  • Utiliser TypeScript ? → Oui

Naviguez dans le projet et installez les dépendances :

cd task-manager
npm install

Lancez le serveur de développement :

npm run dev

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

Cré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 navigation
  • action() — 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 serveur
  • throw redirect("/") — Après une mutation, redirige. Dans SolidStart, les redirections depuis les actions utilisent throw

É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

  1. route.preload — Quand un utilisateur navigue vers cette page, SolidStart commence à récupérer les données en avance
  2. createAsync — Crée une ressource réactive qui suspend le rendu jusqu'à ce que les données soient prêtes
  3. <For> — Le composant de boucle optimisé de Solid. Il ne re-rend que les éléments individuels qui changent
  4. <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 :

  1. SolidStart intercepte la soumission du formulaire
  2. Sérialise le FormData
  3. L'envoie à la fonction serveur
  4. La fonction serveur traite les données et redirige
  5. Tous les caches query associé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/42 associe id à "42"
  • useParams() — Accède aux paramètres dynamiques de manière réactive
  • createSignal — L'équivalent Solid de useState de 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.mjs

Déployer sur d'autres plateformes

PlateformePreset
Vercelvercel
Netlifynetlify
Cloudflare Pagescloudflare-pages
AWS Lambdaaws-lambda
Node.jsnode-server
Statique (SSG)static

Tester votre implémentation

Vérifiez que tout fonctionne :

  1. Créer des tâches — Naviguez vers /tasks/new, remplissez le titre et la description, soumettez
  2. Voir les tâches — La page d'accueil affiche toutes les tâches avec les statistiques
  3. Filtrer les tâches — Cliquez "Actives" ou "Terminées" pour filtrer
  4. Basculer l'état — Cliquez la case à cocher pour marquer les tâches terminées/actives
  5. Voir les détails — Cliquez le titre d'une tâche pour voir sa page de détail
  6. Modifier les tâches — Sur la page de détail, cliquez "Modifier"
  7. Supprimer les tâches — Cliquez "Supprimer" pour retirer une tâche
  8. Tester l'API — Utilisez curl pour 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éSolidStartNext.jsSvelteKitNuxt
RéactivitéSignaux finsDOM virtuelCompilationDOM virtuel (Vue)
Taille du bundle~7 Ko~85 Ko~15 Ko~60 Ko
Fonctions serveur"use server"Server ActionsForm actionsRépertoire server/
Chargement donnéesquery + createAsyncfetch dans RSCFonctions loaduseFetch

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 query et createAsync
  • Les mutations basées sur les formulaires avec action et 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Construire une Application Multi-Tenant avec Next.js.

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