Construire une application full-stack avec TanStack Start : le framework React nouvelle génération

TanStack Start est le nouveau meta-framework React qui bouleverse l'écosystème. Construit sur TanStack Router et Vite, il offre un routage type-safe, des fonctions serveur, du SSR en streaming et un déploiement universel — le tout avec une expérience développeur exceptionnelle. Dans ce tutoriel, vous allez construire une application full-stack de A à Z.
Ce que vous allez construire
Une application TaskBoard — un gestionnaire de tâches collaboratif comprenant :
- Routage fichier type-safe avec TanStack Router
- Fonctions serveur pour les opérations CRUD
- SSR (Server-Side Rendering) avec streaming
- Validation des entrées avec Zod
- Middleware d'authentification
- Persistance des données avec Prisma et SQLite
- Interface responsive avec Tailwind CSS
- Déploiement sur Vercel
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- npm ou pnpm comme gestionnaire de paquets
- Des connaissances de base en React et TypeScript
- Un éditeur de code (VS Code recommandé)
- Des notions de base sur les API REST
Pourquoi TanStack Start ?
Avant de plonger dans le code, comprenons pourquoi TanStack Start se démarque en 2026 :
| Caractéristique | TanStack Start | Next.js | Remix |
|---|---|---|---|
| Routage | Type-safe, fichier | Fichier | Fichier |
| Moteur de build | Vite | Webpack/Turbopack | Vite |
| Fonctions serveur | createServerFn | Server Actions | action/loader |
| SSR Streaming | Oui | Oui | Oui |
| ISR | Oui | Oui | Non |
| Mode SPA | Oui | Non | Non |
| Type-safety | De bout en bout | Partiel | Partiel |
TanStack Start adopte une philosophie client-first : vous écrivez du React classique, et le framework gère la complexité du serveur de manière transparente.
Étape 1 : Initialiser le projet
Commencez par créer un nouveau projet TanStack Start à partir du template officiel :
npx gitpick TanStack/router/tree/main/examples/react/start-basic taskboard
cd taskboard
npm installVérifiez que tout fonctionne :
npm run devVotre application devrait être accessible sur http://localhost:3000.
Structure du projet
Voici la structure de base que nous allons utiliser :
taskboard/
├── app/
│ ├── routes/
│ │ ├── __root.tsx # Layout racine
│ │ ├── index.tsx # Page d'accueil
│ │ ├── tasks.tsx # Liste des tâches
│ │ ├── tasks.$taskId.tsx # Détail d'une tâche
│ │ └── api/
│ │ └── tasks.ts # Route API
│ ├── components/
│ │ ├── TaskCard.tsx
│ │ ├── TaskForm.tsx
│ │ └── Header.tsx
│ ├── lib/
│ │ ├── db.ts # Client Prisma
│ │ └── server-fns.ts # Fonctions serveur
│ ├── client.tsx # Point d'entrée client
│ ├── router.tsx # Configuration du routeur
│ └── ssr.tsx # Point d'entrée SSR
├── prisma/
│ └── schema.prisma
├── app.config.ts # Configuration TanStack Start
├── tailwind.config.ts
└── package.json
Étape 2 : Configurer TanStack Start
Le fichier app.config.ts est le cœur de la configuration :
// app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});Cette configuration minimaliste tire parti de Vite sous le capot, ce qui signifie des temps de démarrage ultra-rapides et du HMR (Hot Module Replacement) instantané.
Étape 3 : Configurer la base de données avec Prisma
Installez Prisma et initialisez-le avec SQLite pour la simplicité :
npm install prisma @prisma/client
npx prisma init --datasource-provider sqliteDéfinissez le schéma de données :
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Task {
id String @id @default(cuid())
title String
description String?
status String @default("todo") // todo, in_progress, done
priority String @default("medium") // low, medium, high
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId String
author User @relation(fields: [authorId], references: [id])
}
model User {
id String @id @default(cuid())
name String
email String @unique
tasks Task[]
createdAt DateTime @default(now())
}Créez le fichier .env :
DATABASE_URL="file:./dev.db"Appliquez le schéma et générez le client :
npx prisma db push
npx prisma generateCréez le client Prisma réutilisable :
// app/lib/db.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}Étape 4 : Comprendre les fonctions serveur
Les fonctions serveur sont le concept clé de TanStack Start. Elles permettent d'exécuter du code serveur de manière type-safe depuis n'importe où dans votre application.
Principe de fonctionnement
[Client] → appel de fonction → [Réseau (RPC)] → [Serveur] → résultat → [Client]
Le build remplace automatiquement le code serveur par des stubs RPC côté client. Votre code serveur ne quitte jamais le serveur.
Syntaxe de base
import { createServerFn } from "@tanstack/react-start";
const maFonctionServeur = createServerFn({ method: "GET" })
.validator((data: unknown) => {
// Validation des entrées
return data as { id: string };
})
.handler(async ({ data }) => {
// Ce code s'exécute uniquement sur le serveur
return { result: "données du serveur" };
});Créez les fonctions serveur de TaskBoard
// app/lib/server-fns.ts
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { prisma } from "./db";
// Schémas de validation
const createTaskSchema = z.object({
title: z.string().min(1, "Le titre est requis").max(200),
description: z.string().optional(),
priority: z.enum(["low", "medium", "high"]),
authorId: z.string(),
});
const updateTaskSchema = z.object({
id: z.string(),
title: z.string().min(1).max(200).optional(),
description: z.string().optional(),
status: z.enum(["todo", "in_progress", "done"]).optional(),
priority: z.enum(["low", "medium", "high"]).optional(),
});
// Récupérer toutes les tâches
export const getTasks = createServerFn({ method: "GET" }).handler(async () => {
const tasks = await prisma.task.findMany({
include: { author: true },
orderBy: { createdAt: "desc" },
});
return tasks;
});
// Récupérer une tâche par ID
export const getTaskById = createServerFn({ method: "GET" })
.validator((data: unknown) => {
const parsed = z.object({ id: z.string() }).parse(data);
return parsed;
})
.handler(async ({ data }) => {
const task = await prisma.task.findUnique({
where: { id: data.id },
include: { author: true },
});
if (!task) {
throw new Error("Tâche introuvable");
}
return task;
});
// Créer une nouvelle tâche
export const createTask = createServerFn({ method: "POST" })
.validator((data: unknown) => createTaskSchema.parse(data))
.handler(async ({ data }) => {
const task = await prisma.task.create({
data: {
title: data.title,
description: data.description,
priority: data.priority,
authorId: data.authorId,
},
include: { author: true },
});
return task;
});
// Mettre à jour une tâche
export const updateTask = createServerFn({ method: "POST" })
.validator((data: unknown) => updateTaskSchema.parse(data))
.handler(async ({ data }) => {
const { id, ...updateData } = data;
const task = await prisma.task.update({
where: { id },
data: updateData,
include: { author: true },
});
return task;
});
// Supprimer une tâche
export const deleteTask = createServerFn({ method: "POST" })
.validator((data: unknown) => {
return z.object({ id: z.string() }).parse(data);
})
.handler(async ({ data }) => {
await prisma.task.delete({ where: { id: data.id } });
return { success: true };
});Point clé : La méthode GET permet la mise en cache et la déduplication automatique des requêtes. Utilisez POST pour les mutations (création, mise à jour, suppression).
Étape 5 : Configurer le routage fichier
TanStack Start utilise un système de routage basé sur les fichiers avec une particularité majeure : il est entièrement type-safe.
Le layout racine
// app/routes/__root.tsx
import {
Outlet,
createRootRoute,
HeadContent,
Scripts,
} from "@tanstack/react-router";
import Header from "../components/Header";
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "TaskBoard — Gestionnaire de tâches" },
],
links: [
{
rel: "stylesheet",
href: "/src/app.css",
},
],
}),
component: RootComponent,
});
function RootComponent() {
return (
<html lang="fr">
<head>
<HeadContent />
</head>
<body className="bg-gray-50 text-gray-900 min-h-screen">
<Header />
<main className="container mx-auto px-4 py-8">
<Outlet />
</main>
<Scripts />
</body>
</html>
);
}La page d'accueil
// app/routes/index.tsx
import { createFileRoute, Link } from "@tanstack/react-router";
import { getTasks } from "../lib/server-fns";
export const Route = createFileRoute("/")({
loader: async () => {
const tasks = await getTasks();
return { tasks };
},
component: HomePage,
});
function HomePage() {
const { tasks } = Route.useLoaderData();
const todoCount = tasks.filter((t) => t.status === "todo").length;
const inProgressCount = tasks.filter(
(t) => t.status === "in_progress"
).length;
const doneCount = tasks.filter((t) => t.status === "done").length;
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">Tableau de bord</h1>
<p className="text-gray-600">
Gérez vos tâches efficacement avec TaskBoard.
</p>
</div>
{/* Statistiques */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<StatCard label="À faire" count={todoCount} color="bg-yellow-100 text-yellow-800" />
<StatCard label="En cours" count={inProgressCount} color="bg-blue-100 text-blue-800" />
<StatCard label="Terminées" count={doneCount} color="bg-green-100 text-green-800" />
</div>
{/* Actions */}
<div className="flex gap-4">
<Link
to="/tasks"
className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium"
>
Voir toutes les tâches
</Link>
</div>
</div>
);
}
function StatCard({
label,
count,
color,
}: {
label: string;
count: number;
color: string;
}) {
return (
<div className={`rounded-xl p-6 ${color}`}>
<p className="text-sm font-medium opacity-80">{label}</p>
<p className="text-3xl font-bold mt-1">{count}</p>
</div>
);
}Remarquez comment Route.useLoaderData() est entièrement typé — TypeScript connaît la structure exacte des données retournées par le loader.
La page des tâches
// app/routes/tasks.tsx
import { createFileRoute, Link, useRouter } from "@tanstack/react-router";
import { getTasks, updateTask, deleteTask } from "../lib/server-fns";
import TaskCard from "../components/TaskCard";
export const Route = createFileRoute("/tasks")({
loader: async () => {
const tasks = await getTasks();
return { tasks };
},
component: TasksPage,
});
function TasksPage() {
const { tasks } = Route.useLoaderData();
const router = useRouter();
const handleStatusChange = async (id: string, status: string) => {
await updateTask({ data: { id, status: status as "todo" | "in_progress" | "done" } });
router.invalidate();
};
const handleDelete = async (id: string) => {
if (confirm("Supprimer cette tâche ?")) {
await deleteTask({ data: { id } });
router.invalidate();
}
};
const columns = [
{ key: "todo", label: "À faire", color: "border-yellow-400" },
{ key: "in_progress", label: "En cours", color: "border-blue-400" },
{ key: "done", label: "Terminé", color: "border-green-400" },
];
return (
<div>
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Tâches</h1>
<Link
to="/tasks/new"
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
+ Nouvelle tâche
</Link>
</div>
{/* Vue Kanban */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{columns.map((col) => (
<div key={col.key} className={`border-t-4 ${col.color} bg-white rounded-lg p-4`}>
<h2 className="font-semibold text-lg mb-4">{col.label}</h2>
<div className="space-y-3">
{tasks
.filter((t) => t.status === col.key)
.map((task) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={handleStatusChange}
onDelete={handleDelete}
/>
))}
</div>
</div>
))}
</div>
</div>
);
}Route dynamique : détail d'une tâche
// app/routes/tasks.$taskId.tsx
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { getTaskById, updateTask } from "../lib/server-fns";
export const Route = createFileRoute("/tasks/$taskId")({
loader: async ({ params }) => {
const task = await getTaskById({ data: { id: params.taskId } });
return { task };
},
component: TaskDetailPage,
});
function TaskDetailPage() {
const { task } = Route.useLoaderData();
const router = useRouter();
const { taskId } = Route.useParams();
const priorityColors = {
low: "bg-gray-100 text-gray-700",
medium: "bg-yellow-100 text-yellow-700",
high: "bg-red-100 text-red-700",
};
const statusLabels = {
todo: "À faire",
in_progress: "En cours",
done: "Terminé",
};
return (
<div className="max-w-2xl mx-auto">
<div className="bg-white rounded-xl shadow-sm p-8">
<div className="flex items-start justify-between mb-6">
<h1 className="text-2xl font-bold">{task.title}</h1>
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
priorityColors[task.priority as keyof typeof priorityColors]
}`}
>
{task.priority}
</span>
</div>
{task.description && (
<p className="text-gray-600 mb-6">{task.description}</p>
)}
<div className="flex items-center gap-4 text-sm text-gray-500 mb-6">
<span>Par {task.author.name}</span>
<span>•</span>
<span>
{new Date(task.createdAt).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
})}
</span>
</div>
{/* Sélecteur de statut */}
<div className="flex gap-2">
{(["todo", "in_progress", "done"] as const).map((status) => (
<button
key={status}
onClick={async () => {
await updateTask({ data: { id: taskId, status } });
router.invalidate();
}}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
task.status === status
? "bg-indigo-600 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
{statusLabels[status]}
</button>
))}
</div>
</div>
</div>
);
}Le paramètre $taskId dans le nom du fichier (tasks.$taskId.tsx) est automatiquement extrait et typé — Route.useParams() retourne { taskId: string } sans aucune configuration supplémentaire.
Étape 6 : Créer les composants UI
Le composant Header
// app/components/Header.tsx
import { Link } from "@tanstack/react-router";
export default function Header() {
return (
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
<Link to="/" className="text-xl font-bold text-indigo-600">
TaskBoard
</Link>
<nav className="flex items-center gap-6">
<Link
to="/"
className="text-gray-600 hover:text-gray-900 transition-colors"
activeProps={{ className: "text-indigo-600 font-medium" }}
>
Accueil
</Link>
<Link
to="/tasks"
className="text-gray-600 hover:text-gray-900 transition-colors"
activeProps={{ className: "text-indigo-600 font-medium" }}
>
Tâches
</Link>
</nav>
</div>
</header>
);
}Le composant TaskCard
// app/components/TaskCard.tsx
interface Task {
id: string;
title: string;
description: string | null;
priority: string;
status: string;
author: { name: string };
}
interface TaskCardProps {
task: Task;
onStatusChange: (id: string, status: string) => void;
onDelete: (id: string) => void;
}
export default function TaskCard({ task, onStatusChange, onDelete }: TaskCardProps) {
const priorityDot = {
low: "bg-gray-400",
medium: "bg-yellow-400",
high: "bg-red-400",
};
const nextStatus: Record<string, string> = {
todo: "in_progress",
in_progress: "done",
done: "todo",
};
return (
<div className="bg-gray-50 rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-2">
<h3 className="font-medium text-sm">{task.title}</h3>
<span
className={`w-2 h-2 rounded-full mt-1.5 ${
priorityDot[task.priority as keyof typeof priorityDot]
}`}
/>
</div>
{task.description && (
<p className="text-xs text-gray-500 mb-3 line-clamp-2">
{task.description}
</p>
)}
<div className="flex items-center justify-between">
<span className="text-xs text-gray-400">{task.author.name}</span>
<div className="flex gap-1">
<button
onClick={() => onStatusChange(task.id, nextStatus[task.status])}
className="text-xs px-2 py-1 bg-indigo-50 text-indigo-600 rounded hover:bg-indigo-100 transition-colors"
>
Avancer
</button>
<button
onClick={() => onDelete(task.id)}
className="text-xs px-2 py-1 bg-red-50 text-red-600 rounded hover:bg-red-100 transition-colors"
>
Supprimer
</button>
</div>
</div>
</div>
);
}Le formulaire de création
// app/components/TaskForm.tsx
import { useState } from "react";
import { useRouter } from "@tanstack/react-router";
import { createTask } from "../lib/server-fns";
export default function TaskForm({ authorId }: { authorId: string }) {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
const formData = new FormData(e.currentTarget);
await createTask({
data: {
title: formData.get("title") as string,
description: (formData.get("description") as string) || undefined,
priority: formData.get("priority") as "low" | "medium" | "high",
authorId,
},
});
router.invalidate();
router.navigate({ to: "/tasks" });
};
return (
<form onSubmit={handleSubmit} className="space-y-6 max-w-lg">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
Titre *
</label>
<input
id="title"
name="title"
type="text"
required
maxLength={200}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="Ex: Refactoriser le module d'authentification"
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
id="description"
name="description"
rows={4}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="Décrivez les détails de cette tâche..."
/>
</div>
<div>
<label htmlFor="priority" className="block text-sm font-medium text-gray-700 mb-1">
Priorité
</label>
<select
id="priority"
name="priority"
defaultValue="medium"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
>
<option value="low">Basse</option>
<option value="medium">Moyenne</option>
<option value="high">Haute</option>
</select>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? "Création en cours..." : "Créer la tâche"}
</button>
</form>
);
}Étape 7 : Ajouter le middleware
Le middleware de TanStack Start permet d'intercepter et de modifier le comportement des fonctions serveur et des routes. C'est idéal pour l'authentification, la journalisation et la validation.
// app/lib/middleware.ts
import { createMiddleware } from "@tanstack/react-start";
// Middleware de journalisation
export const loggingMiddleware = createMiddleware().server(
async ({ next }) => {
const start = Date.now();
const result = await next();
const duration = Date.now() - start;
console.log(`[Server] Requête traitée en ${duration}ms`);
return result;
}
);
// Middleware d'authentification
export const authMiddleware = createMiddleware()
.server(async ({ next }) => {
// Vérifiez le token d'authentification ici
// Par exemple, lire un cookie de session
const userId = "user-demo-id"; // Remplacez par votre logique d'auth
if (!userId) {
throw new Error("Non authentifié");
}
return next({ context: { userId } });
});Utilisez le middleware dans vos fonctions serveur :
// Exemple d'utilisation avec le middleware
export const createTaskAuthenticated = createServerFn({ method: "POST" })
.middleware([authMiddleware, loggingMiddleware])
.validator((data: unknown) => createTaskSchema.parse(data))
.handler(async ({ data, context }) => {
// context.userId est disponible grâce au middleware d'auth
const task = await prisma.task.create({
data: {
...data,
authorId: context.userId,
},
});
return task;
});Le middleware est composable — vous pouvez chaîner plusieurs middlewares et chacun peut enrichir le contexte transmis au suivant.
Étape 8 : Routes API
TanStack Start permet aussi de créer des routes API classiques pour les intégrations externes :
// app/routes/api/tasks.ts
import { json } from "@tanstack/react-start";
import { createAPIFileRoute } from "@tanstack/react-start/api";
import { prisma } from "../../lib/db";
export const APIRoute = createAPIFileRoute("/api/tasks")({
GET: async () => {
const tasks = await prisma.task.findMany({
include: { author: true },
orderBy: { createdAt: "desc" },
});
return json(tasks);
},
POST: async ({ request }) => {
const body = await request.json();
const task = await prisma.task.create({
data: body,
include: { author: true },
});
return json(task, { status: 201 });
},
});Ces routes API sont accessibles via GET /api/tasks et POST /api/tasks, ce qui est utile pour les webhooks, les applications mobiles ou les intégrations tierces.
Étape 9 : Gestion des erreurs
TanStack Start propose une gestion d'erreurs élégante via les Error Boundaries :
// app/routes/tasks.$taskId.tsx (ajout d'errorComponent)
export const Route = createFileRoute("/tasks/$taskId")({
loader: async ({ params }) => {
const task = await getTaskById({ data: { id: params.taskId } });
return { task };
},
component: TaskDetailPage,
errorComponent: TaskError,
notFoundComponent: TaskNotFound,
});
function TaskError({ error }: { error: Error }) {
return (
<div className="max-w-md mx-auto text-center py-12">
<div className="text-red-500 text-5xl mb-4">!</div>
<h2 className="text-xl font-bold mb-2">Une erreur est survenue</h2>
<p className="text-gray-600">{error.message}</p>
</div>
);
}
function TaskNotFound() {
return (
<div className="max-w-md mx-auto text-center py-12">
<div className="text-gray-400 text-5xl mb-4">?</div>
<h2 className="text-xl font-bold mb-2">Tâche introuvable</h2>
<p className="text-gray-600">
Cette tâche n'existe pas ou a été supprimée.
</p>
</div>
);
}Étape 10 : Prérendu statique et ISR
TanStack Start supporte le prérendu statique et l'ISR (Incremental Static Regeneration) pour des performances optimales :
// app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
server: {
prerender: {
routes: ["/"],
crawlLinks: true,
},
},
});Pour l'ISR sur des routes spécifiques :
// app/routes/index.tsx
export const Route = createFileRoute("/")({
loader: async () => {
const tasks = await getTasks();
return { tasks };
},
// Revalider toutes les 60 secondes
headers: () => ({
"Cache-Control": "public, max-age=60, stale-while-revalidate=120",
}),
component: HomePage,
});Étape 11 : Déployer sur Vercel
TanStack Start s'intègre parfaitement avec Vercel. Installez le preset :
npm install @tanstack/react-start-preset-vercelMettez à jour la configuration :
// app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
server: {
preset: "vercel",
},
});Déployez :
npm i -g vercel
vercelVercel détectera automatiquement TanStack Start et configurera le build. Votre application sera accessible en quelques minutes.
Autres options de déploiement
TanStack Start supporte également :
- Cloudflare Workers — avec
@tanstack/react-start-preset-cloudflare - Netlify — avec
@tanstack/react-start-preset-netlify - Railway — configuration Docker standard
- Node.js standalone — pour un hébergement classique
Tester votre application
Seed de la base de données
Créez un script de seed pour tester avec des données :
// prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
const user = await prisma.user.create({
data: {
name: "Ahmed Mansouri",
email: "ahmed@example.com",
},
});
const tasks = [
{
title: "Configurer l'environnement de développement",
description: "Installer Node.js, Docker et les extensions VS Code nécessaires.",
status: "done",
priority: "high",
authorId: user.id,
},
{
title: "Implémenter l'authentification OAuth",
description: "Ajouter le support Google et GitHub OAuth avec Better Auth.",
status: "in_progress",
priority: "high",
authorId: user.id,
},
{
title: "Écrire les tests unitaires",
description: "Couvrir les fonctions serveur avec Vitest.",
status: "todo",
priority: "medium",
authorId: user.id,
},
{
title: "Optimiser les performances du dashboard",
description: "Profiler le rendu et appliquer React.memo si nécessaire.",
status: "todo",
priority: "low",
authorId: user.id,
},
];
for (const task of tasks) {
await prisma.task.create({ data: task });
}
console.log("Base de données peuplée avec succès !");
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());Exécutez le seed :
npx tsx prisma/seed.tsVérification
Lancez l'application et vérifiez :
- La page d'accueil affiche les statistiques
- La page
/tasksmontre les colonnes Kanban - La création de tâche fonctionne via le formulaire
- Le changement de statut met à jour la vue
- La suppression fonctionne avec confirmation
- Les routes API répondent sur
/api/tasks
Dépannage
Erreurs courantes
"Cannot find module '@prisma/client'"
npx prisma generate"Port 3000 already in use"
npm run dev -- --port 3001"Type error in Route.useLoaderData()"
Assurez-vous que le fichier routeTree.gen.ts est à jour :
npm run devTanStack Start régénère automatiquement l'arbre de routes au démarrage.
Le HMR ne fonctionne pas
Vérifiez que votre fichier app.config.ts est correct et que Vite est bien configuré. Redémarrez le serveur de développement si nécessaire.
Pour aller plus loin
Maintenant que vous maîtrisez les bases de TanStack Start, voici des pistes pour approfondir :
- Authentification complète : intégrez Better Auth ou Lucia pour une gestion des sessions robuste
- Temps réel : ajoutez des WebSockets avec Socket.io pour les mises à jour en temps réel du tableau Kanban
- Tests : utilisez Vitest pour tester vos fonctions serveur et Playwright pour les tests end-to-end
- Internationalisation : explorez le support i18n avec Paraglide ou next-intl adapté à TanStack Start
- Monitoring : intégrez Sentry pour le suivi des erreurs en production
Conclusion
Dans ce tutoriel, vous avez appris à :
- Initialiser un projet TanStack Start avec Vite et TypeScript
- Configurer une base de données avec Prisma et SQLite
- Créer des fonctions serveur type-safe avec validation Zod
- Implémenter le routage fichier avec des routes statiques et dynamiques
- Construire une UI avec des composants React et Tailwind CSS
- Ajouter du middleware pour l'authentification et la journalisation
- Créer des routes API pour les intégrations externes
- Gérer les erreurs avec les Error Boundaries
- Déployer sur Vercel et d'autres plateformes
TanStack Start représente une évolution significative dans l'écosystème React. Sa philosophie client-first, combinée à un routage entièrement type-safe et des fonctions serveur élégantes, en fait un choix de premier plan pour les applications full-stack en 2026.
Le code source complet de ce tutoriel est disponible sur GitHub comme point de départ pour vos propres projets.
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

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
Apprenez à construire des agents IA depuis zéro avec TypeScript. Ce tutoriel couvre le pattern ReAct, l'appel d'outils, le raisonnement multi-étapes et les boucles d'agents prêtes pour la production avec le Vercel AI SDK.