A modern React starter template for building visually impressive demos quickly. Built with TanStack Start, React 19, shadcn/ui, Tailwind CSS v4, and Claude.
- 🚀 React 19 with functional components and hooks
- 🎯 TanStack Start for full-stack React with client-side rendering
- 🧭 TanStack Router with file-based routing
- 📦 TanStack Store for state management
- 🔄 TanStack Query for data fetching and caching
- 🔌 Server Functions for type-safe server-side logic
- 🗄️ Drizzle ORM with Neon Postgres and drizzle-zod integration
- ☁️ Neon Database with branching for development
- 🎨 Tailwind CSS v4 with semantic color variables
- 🧩 shadcn/ui components (new-york style)
- 🤖 AI SDK with Anthropic Claude
- ✨ Motion for smooth animations and transitions
- 📝 TypeScript with strict mode and path aliases
- Node.js v22.12.0+ (Download Node.js)
- pnpm (
npm install -g pnpm) - GitHub and Vercel accounts
- Vercel CLI (
npm install -g vercel) - Anthropic API key (console.anthropic.com)
- Click the Deploy with Vercel button above
- Sign in to Vercel and GitHub if needed
- You will be prompted to configure details on your project setup:
- Choose GitHub as your git provider, select your git workspace, and assign a name to your repo
- Choose your Vercel team and click Create
- In Add Products, you should see Neon. Click Add and configure the following settings:
- Keep Region as the default
- Set Auth to
false, click "Continue" - Keep Database Name (*) as the default name, click "Create"
- Check the Preview box for the Create Database Branch For Deployment section, keep everything else as is. Click "Connect"
- Enter your
ANTHROPIC_API_KEYwhen prompted - Click Deploy
This will:
- Clone the repo to your GitHub account
- Create a Vercel project
- Provision a Neon Postgres database with branching
- Deploy your app
Once deployment has completed, go to the project in Vercel and click the Repository button to navigate to the repository in GitHub
git clone <your-repo-url>
cd <your-repo>pnpm installvercel link # Select your deployed project when promptedWhen prompted to pull environment variables now, select Yes
echo "ANTHROPIC_API_KEY=your_key_here" >> .env.localReplace your_key_here with your actual API key
pnpm db:push # Push schema to Neon
pnpm db:seed # Optional: seed with example datapnpm devVisit http://localhost:3000 to see your app.
src/
├── routes/ # File-based routing
│ ├── __root.tsx # Root layout with Header and Outlet
│ ├── index.tsx # Home page
│ └── example/ # Example routes (safe to delete)
├── routeTree.gen.ts # Auto-generated route tree (don't edit)
├── router.tsx # Router configuration
├── components/ # React components
│ ├── ui/ # shadcn/ui components (managed by CLI)
│ ├── ai-elements/ # AI-powered UI components
│ └── header.tsx # Navigation header
├── db/ # Database layer
│ ├── schema.ts # Database schema (Drizzle ORM + drizzle-zod)
│ ├── client.ts # Database connection
│ └── seed.ts # Database seeding script
├── server/ # Server functions (TanStack Start)
│ └── posts.ts # Example server functions with query options
├── lib/ # Utilities and helpers
│ ├── utils.ts # cn() for className merging
│ ├── env-client.ts # Client-side environment variables
│ ├── env-server.ts # Server-side environment variables
│ └── demo-store.ts # Example TanStack Store (safe to delete)
├── store/ # TanStack Store state management
├── data/ # Static/mock data
├── utils/ # Additional utilities
└── styles.css # Global styles and Tailwind config
migrations/ # Database migrations (auto-generated)
pnpm db:push # push schema directly (dev)
pnpm db:generate # generate migration from schema changes
pnpm db:migrate # apply migrations to database
pnpm db:seed # seed database with example data
pnpm db:studio # open Drizzle Studio GUIpnpm dev # start dev server
pnpm build # build for productionpnpm lint:types path/to/file.tsx # type check
pnpm lint:eslint path/to/file.tsx # lint
pnpm format:prettier path/to/file.tsx # formatpnpm lint # run all lints
pnpm lint:types # type check all files
pnpm lint:eslint # lint all files
pnpm lint:format # check formatting
pnpm lint:knip # check for unused codeUse Tailwind CSS v4 with semantic color variables from src/styles.css:
<div className="border border-border bg-primary text-foreground">
<button className="bg-accent hover:bg-accent/90">Click me</button>
</div>Always use cn() to merge className strings:
import { cn } from "@/lib/utils";
<div className={cn("base-class", isActive && "active-class")} />;Create a file in src/routes/:
// src/routes/about.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/about")({
component: AboutPage,
});
function AboutPage() {
return <div>About Page</div>;
}1. Define schema in src/db/schema.ts:
// src/db/schema.ts
import { pgTable, serial, text } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import * as z from "zod";
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
content: text("content").notNull(),
});
// Auto-generate Zod schemas from Drizzle
export const selectPostSchema = createSelectSchema(posts);
export const insertPostSchema = createInsertSchema(posts);
export type Post = z.infer<typeof selectPostSchema>;2. Create server function in src/server/posts.ts:
// src/server/posts.ts
import { queryOptions } from "@tanstack/react-query";
import { createServerFn } from "@tanstack/react-start";
import { db } from "@/db/client";
import { posts, type Post } from "@/db/schema";
export const listPosts = createServerFn({ method: "GET" }).handler(
async (): Promise<Post[]> => {
return await db.select().from(posts);
},
);
export const listPostsQueryOptions = () =>
queryOptions({
queryKey: ["posts"],
queryFn: () => listPosts(),
});3. Use in route with loader and useSuspenseQuery:
// src/routes/posts/index.tsx
import { useSuspenseQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import { listPostsQueryOptions } from "@/server/posts";
export const Route = createFileRoute("/posts/")({
loader: ({ context }) =>
context.queryClient.ensureQueryData(listPostsQueryOptions()),
component: PostsPage,
});
function PostsPage() {
const { data: posts } = useSuspenseQuery(listPostsQueryOptions());
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}import { useStore } from "@tanstack/react-store";
import { Store } from "@tanstack/store";
export const counterStore = new Store(0);
function Counter() {
const count = useStore(counterStore);
return (
<button onClick={() => counterStore.setState((n) => n + 1)}>
Count: {count}
</button>
);
}Server route (src/routes/api.chat.ts):
import { anthropic } from "@ai-sdk/anthropic";
import { createFileRoute } from "@tanstack/react-router";
import { convertToModelMessages, streamText, UIMessage } from "ai";
export const Route = createFileRoute("/api/chat")({
server: {
handlers: {
POST: async ({ request }) => {
const { messages }: { messages: UIMessage[] } = await request.json();
const result = streamText({
model: anthropic("claude-sonnet-4-5-20250929"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
},
},
},
});Client usage:
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
function Chat() {
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
// ...
}See src/routes/example/ for complete working examples.
src/routes/example/chat.tsx- AI chat interface with streamingsrc/routes/example/posts/- Dynamic routes with Drizzle + Server Functionssrc/server/posts.ts- Server functions with query optionssrc/routes/example/store.tsx- TanStack Store with derived statesrc/lib/demo-store.ts- Example store (safe to delete)
You can safely delete src/routes/example/ and src/lib/demo-store.ts when starting your project.