Building a Full-Stack App with TanStack Start: The Next-Generation React Framework

TanStack Start is the new React meta-framework shaking up the ecosystem. Built on TanStack Router and Vite, it offers type-safe routing, server functions, streaming SSR, and universal deployment — all with an exceptional developer experience. In this tutorial, you will build a full-stack application from scratch.
What You'll Build
A TaskBoard application — a collaborative task manager featuring:
- Type-safe file-based routing with TanStack Router
- Server functions for CRUD operations
- SSR (Server-Side Rendering) with streaming
- Input validation with Zod
- Authentication middleware
- Data persistence with Prisma and SQLite
- Responsive interface with Tailwind CSS
- Deployment to Vercel
Prerequisites
Before getting started, make sure you have:
- Node.js 20+ installed
- npm or pnpm as a package manager
- Basic knowledge of React and TypeScript
- A code editor (VS Code recommended)
- Basic understanding of REST APIs
Why TanStack Start?
Before diving into the code, let's understand why TanStack Start stands out in 2026:
| Feature | TanStack Start | Next.js | Remix |
|---|---|---|---|
| Routing | Type-safe, file-based | File-based | File-based |
| Build engine | Vite | Webpack/Turbopack | Vite |
| Server functions | createServerFn | Server Actions | action/loader |
| Streaming SSR | Yes | Yes | Yes |
| ISR | Yes | Yes | No |
| SPA Mode | Yes | No | No |
| Type-safety | End-to-end | Partial | Partial |
TanStack Start adopts a client-first philosophy: you write standard React, and the framework handles the server complexity transparently.
Step 1: Initialize the Project
Start by creating a new TanStack Start project from the official template:
npx gitpick TanStack/router/tree/main/examples/react/start-basic taskboard
cd taskboard
npm installVerify that everything works:
npm run devYour application should be accessible at http://localhost:3000.
Project Structure
Here is the base structure we will use:
taskboard/
├── app/
│ ├── routes/
│ │ ├── __root.tsx # Root layout
│ │ ├── index.tsx # Home page
│ │ ├── tasks.tsx # Task list
│ │ ├── tasks.$taskId.tsx # Task detail
│ │ └── api/
│ │ └── tasks.ts # API route
│ ├── components/
│ │ ├── TaskCard.tsx
│ │ ├── TaskForm.tsx
│ │ └── Header.tsx
│ ├── lib/
│ │ ├── db.ts # Prisma client
│ │ └── server-fns.ts # Server functions
│ ├── client.tsx # Client entry point
│ ├── router.tsx # Router configuration
│ └── ssr.tsx # SSR entry point
├── prisma/
│ └── schema.prisma
├── app.config.ts # TanStack Start configuration
├── tailwind.config.ts
└── package.json
Step 2: Configure TanStack Start
The app.config.ts file is the heart of the configuration:
// app.config.ts
import { defineConfig } from "@tanstack/react-start/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});This minimal configuration leverages Vite under the hood, which means ultra-fast startup times and instant HMR (Hot Module Replacement).
Step 3: Set Up the Database with Prisma
Install Prisma and initialize it with SQLite for simplicity:
npm install prisma @prisma/client
npx prisma init --datasource-provider sqliteDefine the data schema:
// 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())
}Create the .env file:
DATABASE_URL="file:./dev.db"Apply the schema and generate the client:
npx prisma db push
npx prisma generateCreate the reusable Prisma client:
// 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;
}Step 4: Understanding Server Functions
Server functions are the key concept in TanStack Start. They allow you to execute server code in a type-safe manner from anywhere in your application.
How It Works
[Client] → function call → [Network (RPC)] → [Server] → result → [Client]
The build automatically replaces server code with RPC stubs on the client side. Your server code never leaves the server.
Basic Syntax
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" };
});Create the TaskBoard Server Functions
// 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 };
});Key point: The GET method enables automatic caching and request deduplication. Use POST for mutations (create, update, delete).
Step 5: Configure File-Based Routing
TanStack Start uses a file-based routing system with one major distinction: it is entirely type-safe.
The Root Layout
// 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>
);
}The Home Page
// 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>
);
}Notice how Route.useLoaderData() is fully typed — TypeScript knows the exact structure of the data returned by the loader.
The Tasks Page
// 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>
);
}Dynamic Route: Task Detail
// 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>
);
}The $taskId parameter in the filename (tasks.$taskId.tsx) is automatically extracted and typed — Route.useParams() returns { taskId: string } without any additional configuration.
Step 6: Create UI Components
The Header Component
// 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>
);
}The TaskCard Component
// 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>
);
}The Creation Form
// 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>
);
}Step 7: Add Middleware
TanStack Start's middleware allows you to intercept and modify the behavior of server functions and routes. It is ideal for authentication, logging, and 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 } });
});Use the middleware in your server functions:
// 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;
});Middleware is composable — you can chain multiple middlewares, and each one can enrich the context passed to the next.
Step 8: API Routes
TanStack Start also allows you to create classic API routes for external integrations:
// 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 });
},
});These API routes are accessible via GET /api/tasks and POST /api/tasks, which is useful for webhooks, mobile applications, or third-party integrations.
Step 9: Error Handling
TanStack Start offers elegant error handling via 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>
);
}Step 10: Static Prerendering and ISR
TanStack Start supports static prerendering and ISR (Incremental Static Regeneration) for optimal performance:
// 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,
},
},
});For ISR on specific routes:
// 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,
});Step 11: Deploy to Vercel
TanStack Start integrates seamlessly with Vercel. Install the preset:
npm install @tanstack/react-start-preset-vercelUpdate the 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",
},
});Deploy:
npm i -g vercel
vercelVercel will automatically detect TanStack Start and configure the build. Your application will be accessible within minutes.
Other Deployment Options
TanStack Start also supports:
- Cloudflare Workers — with
@tanstack/react-start-preset-cloudflare - Netlify — with
@tanstack/react-start-preset-netlify - Railway — standard Docker configuration
- Node.js standalone — for traditional hosting
Testing Your Application
Database Seeding
Create a seed script to test with data:
// 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());Run the seed:
npx tsx prisma/seed.tsVerification
Launch the application and verify:
- The home page displays the statistics
- The
/taskspage shows the Kanban columns - Task creation works via the form
- Status changes update the view
- Deletion works with confirmation
- API routes respond on
/api/tasks
Troubleshooting
Common Errors
"Cannot find module '@prisma/client'"
npx prisma generate"Port 3000 already in use"
npm run dev -- --port 3001"Type error in Route.useLoaderData()"
Make sure the routeTree.gen.ts file is up to date:
npm run devTanStack Start automatically regenerates the route tree on startup.
HMR is not working
Check that your app.config.ts file is correct and that Vite is properly configured. Restart the development server if necessary.
Going Further
Now that you have mastered the basics of TanStack Start, here are some avenues to explore further:
- Full authentication: integrate Better Auth or Lucia for robust session management
- Real-time: add WebSockets with Socket.io for real-time Kanban board updates
- Testing: use Vitest to test your server functions and Playwright for end-to-end testing
- Internationalization: explore i18n support with Paraglide or next-intl adapted for TanStack Start
- Monitoring: integrate Sentry for production error tracking
Conclusion
In this tutorial, you learned how to:
- Initialize a TanStack Start project with Vite and TypeScript
- Configure a database with Prisma and SQLite
- Create type-safe server functions with Zod validation
- Implement file-based routing with static and dynamic routes
- Build a UI with React components and Tailwind CSS
- Add middleware for authentication and logging
- Create API routes for external integrations
- Handle errors with Error Boundaries
- Deploy to Vercel and other platforms
TanStack Start represents a significant evolution in the React ecosystem. Its client-first philosophy, combined with fully type-safe routing and elegant server functions, makes it a top choice for full-stack applications in 2026.
The complete source code for this tutorial is available on GitHub as a starting point for your own projects.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Authenticate Your Next.js 15 App with Auth.js v5: Email, OAuth, and Role-Based Access
Learn how to add production-ready authentication to your Next.js 15 application using Auth.js v5. This comprehensive guide covers Google OAuth, email/password credentials, protected routes, middleware, and role-based access control.

Building an Autonomous AI Agent with Agentic RAG and Next.js
Learn how to build an AI agent that autonomously decides when and how to retrieve information from vector databases. A comprehensive hands-on guide using Vercel AI SDK and Next.js with executable examples.

Building AI Agents from Scratch with TypeScript: Master the ReAct Pattern Using the Vercel AI SDK
Learn how to build AI agents from the ground up using TypeScript. This tutorial covers the ReAct pattern, tool calling, multi-step reasoning, and production-ready agent loops with the Vercel AI SDK.