[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Full-Stack App with TanStack Start

Build a Full-Stack App with TanStack Start

Build a full-stack React app with TanStack Start — type-safe server functions, file-based routing, SSR with streaming, and seamless integration with TanStack Router and TanStack Query.

Development#tanstack#react#full-stack#ssr#server-functions
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 4 skills

Avg quality 89/100·All SAFE
>

tanstack-start

v1.1.0

Build full-stack React apps with TanStack Start. Use when a user asks to create a full-stack React application with type-safe server functions, set up file-based routing with SSR/SSG/SPA modes, build APIs with middleware and validation, implement server-side data fetching with TanStack Router, or deploy to Cloudflare/Netlify/Vercel/Node.

100/100 quality
5.80× impact
SAFE· stale score
View skill
>

tanstack-router

v1.1.0

Type-safe routing for React with file-based routes, validated search params, loaders, and automatic code splitting. Use when someone asks to "set up routing for React", "type-safe router", "TanStack Router", "file-based routing", "search params validation", "replace React Router with something type-safe", or "add route-level data loading". Covers file-based routing, search params with Zod, route loaders, code splitting, and layouts.

77/100 quality
4.13× impact
SAFE· stale score
View skill
>

drizzle-orm

v1.1.0

Drizzle ORM is a lightweight, type-safe TypeScript ORM whose query builder maps directly to SQL, with a companion CLI (drizzle-kit) that generates and applies migrations. Use when a user asks to define a database schema in TypeScript, write typed Postgres, MySQL or SQLite queries, run drizzle-kit generate, migrate or push, or connect Drizzle to Neon, Turso, Cloudflare D1, PGlite or node-postgres.

87/100 quality
6.70× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

Kian uses TanStack Router for client routing and TanStack Query for data fetching. But wiring them to a backend means separate API routes, manual type definitions, and duplicated validation. TanStack Start unifies everything: server functions called directly from route loaders, type-safe from database to UI, with SSR and streaming out of the box. It's the missing piece that connects the TanStack ecosystem into a full-stack framework.

Step 1: Project Setup

bash
npx create-start@latest my-app
cd my-app
npm install drizzle-orm @libsql/client zod
npm install -D drizzle-kit

Step 2: Server Functions

typescript
// src/server/functions/todos.ts
import { createServerFn } from "@tanstack/start";
import { z } from "zod";
import { db } from "../db";
import { todos } from "../db/schema";
import { eq, desc } from "drizzle-orm";

export const getTodos = createServerFn({ method: "GET" })
  .validator(z.object({
    filter: z.enum(["all", "active", "completed"]).optional(),
  }))
  .handler(async ({ data }) => {
    const where = data.filter === "active"
      ? eq(todos.completed, false)
      : data.filter === "completed"
        ? eq(todos.completed, true)
        : undefined;

    return db.query.todos.findMany({
      where,
      orderBy: [desc(todos.createdAt)],
    });
  });

export const createTodo = createServerFn({ method: "POST" })
  .validator(z.object({
    title: z.string().min(1).max(500),
  }))
  .handler(async ({ data }) => {
    const [todo] = await db.insert(todos).values({
      id: crypto.randomUUID(),
      title: data.title,
      completed: false,
      createdAt: new Date(),
    }).returning();
    return todo;
  });

export const toggleTodo = createServerFn({ method: "POST" })
  .validator(z.object({ id: z.string().uuid() }))
  .handler(async ({ data }) => {
    const todo = await db.query.todos.findFirst({ where: eq(todos.id, data.id) });
    if (!todo) throw new Error("Not found");
    const [updated] = await db.update(todos)
      .set({ completed: !todo.completed })
      .where(eq(todos.id, data.id))
      .returning();
    return updated;
  });

export const deleteTodo = createServerFn({ method: "POST" })
  .validator(z.object({ id: z.string().uuid() }))
  .handler(async ({ data }) => {
    await db.delete(todos).where(eq(todos.id, data.id));
  });

Step 3: Route with Loader

typescript
// src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { getTodos, createTodo, toggleTodo, deleteTodo } from "../server/functions/todos";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";

export const Route = createFileRoute("/")({
  validateSearch: (search) => ({
    filter: (search.filter as string) || "all",
  }),
  loaderDeps: ({ search }) => ({ filter: search.filter }),
  loader: async ({ deps }) => {
    return getTodos({ data: { filter: deps.filter as any } });
  },
  component: TodoApp,
});

function TodoApp() {
  const todos = Route.useLoaderData();
  const search = Route.useSearch();
  const navigate = Route.useNavigate();
  const queryClient = useQueryClient();
  const [newTitle, setNewTitle] = useState("");

  const addMutation = useMutation({
    mutationFn: (title: string) => createTodo({ data: { title } }),
    onSuccess: () => {
      queryClient.invalidateQueries();
      setNewTitle("");
    },
  });

  const toggleMutation = useMutation({
    mutationFn: (id: string) => toggleTodo({ data: { id } }),
    onSuccess: () => queryClient.invalidateQueries(),
  });

  const deleteMutation = useMutation({
    mutationFn: (id: string) => deleteTodo({ data: { id } }),
    onSuccess: () => queryClient.invalidateQueries(),
  });

  return (
    <main className="max-w-lg mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">Todos</h1>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (newTitle.trim()) addMutation.mutate(newTitle);
        }}
        className="flex gap-2 mb-6"
      >
        <input
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder="What needs to be done?"
          className="flex-1 px-3 py-2 border rounded"
        />
        <button
          type="submit"
          disabled={addMutation.isPending}
          className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
        >
          Add
        </button>
      </form>

      {/* Filter tabs */}
      <div className="flex gap-2 mb-4">
        {["all", "active", "completed"].map((f) => (
          <button
            key={f}
            onClick={() => navigate({ search: { filter: f } })}
            className={`px-3 py-1 rounded ${search.filter === f ? "bg-blue-100 text-blue-700" : "text-gray-600"}`}
          >
            {f.charAt(0).toUpperCase() + f.slice(1)}
          </button>
        ))}
      </div>

      <ul className="space-y-2">
        {todos.map((todo) => (
          <li key={todo.id} className="flex items-center gap-3 p-3 bg-white rounded border">
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleMutation.mutate(todo.id)}
              className="w-5 h-5"
            />
            <span className={`flex-1 ${todo.completed ? "line-through text-gray-400" : ""}`}>
              {todo.title}
            </span>
            <button
              onClick={() => deleteMutation.mutate(todo.id)}
              className="text-red-400 hover:text-red-600"
            >
              ✕
            </button>
          </li>
        ))}
      </ul>

      <p className="text-sm text-gray-500 mt-4">
        {todos.filter((t) => !t.completed).length} items left
      </p>
    </main>
  );
}

Step 4: Authentication Middleware

typescript
// src/server/middleware/auth.ts
import { createMiddleware } from "@tanstack/start";

export const authMiddleware = createMiddleware().server(async ({ next, request }) => {
  const session = await getSession(request);

  if (!session) {
    throw new Error("Unauthorized");
  }

  return next({ context: { user: session.user } });
});

// Use in server functions:
export const getProtectedData = createServerFn({ method: "GET" })
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    // context.user is typed and guaranteed to exist
    return db.query.projects.findMany({
      where: eq(projects.ownerId, context.user.id),
    });
  });

Summary

Kian has a full-stack app where server functions are called directly from route loaders — no API routes to write, no fetch calls to wire up, no type definitions to duplicate. createServerFn validates input with Zod on the server and infers the return type for the client. Route loaders prefetch data during SSR, so the page arrives fully rendered. TanStack Query handles mutations with optimistic updates and cache invalidation. The entire data flow from database to UI is type-checked at compile time — change a database column and TypeScript errors propagate all the way to the component that renders it.

Related use cases