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

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

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:

FeatureTanStack StartNext.jsRemix
RoutingType-safe, file-basedFile-basedFile-based
Build engineViteWebpack/TurbopackVite
Server functionscreateServerFnServer Actionsaction/loader
Streaming SSRYesYesYes
ISRYesYesNo
SPA ModeYesNoNo
Type-safetyEnd-to-endPartialPartial

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 install

Verify that everything works:

npm run dev

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

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

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

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

Vercel 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.ts

Verification

Launch the application and verify:

  1. The home page displays the statistics
  2. The /tasks page shows the Kanban columns
  3. Task creation works via the form
  4. Status changes update the view
  5. Deletion works with confirmation
  6. 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 dev

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

  1. Initialize a TanStack Start project with Vite and TypeScript
  2. Configure a database with Prisma and SQLite
  3. Create type-safe server functions with Zod validation
  4. Implement file-based routing with static and dynamic routes
  5. Build a UI with React components and Tailwind CSS
  6. Add middleware for authentication and logging
  7. Create API routes for external integrations
  8. Handle errors with Error Boundaries
  9. 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.


Want to read more tutorials? Check out our latest tutorial on Mastering Twilio SMS: A Beginner’s Guide to Node.js Messaging for Business.

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