DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on Originally published at stacknotice.com

TanStack Start (2026): Full-Stack React Without Next.js

Next.js has dominated the full-stack React space for years. TanStack Start is the first serious alternative built from scratch for the current era — type-safe routing, server functions with real TypeScript inference, and a deployment model that runs anywhere Nitro runs.

TanStack Start is built on three pieces: TanStack Router for client-side routing, Vinxi as the application bundler, and Nitro as the server engine.

Project Setup

npx create-tsrouter-app@latest my-app --template start-basic
cd my-app
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Project structure:

app/
├── routes/
│   ├── __root.tsx          # Root layout
│   ├── index.tsx           # /
│   └── posts/
│       ├── index.tsx       # /posts
│       └── $postId.tsx     # /posts/:postId — typed param
├── client.tsx
├── router.tsx
└── ssr.tsx
app.config.ts               # Vinxi config — adapters, plugins
Enter fullscreen mode Exit fullscreen mode

File-Based Routing — Typed Params

// app/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { fetchPost } from '../serverFunctions/posts'

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => fetchPost(params.postId),  // params.postId is typed
  component: PostPage,
})

function PostPage() {
  const post = Route.useLoaderData()  // typed from loader return
  const { postId } = Route.useParams()  // typed, no casting

  return <article><h1>{post.title}</h1><p>{post.content}</p></article>
}
Enter fullscreen mode Exit fullscreen mode

The key difference from Next.js: Route.useLoaderData() is typed from the loader's return type automatically.

Server Functions

Server functions run on the server but are callable from the client — with full TypeScript inference on both sides.

// app/serverFunctions/posts.ts
import { createServerFn } from '@tanstack/start'
import { z } from 'zod'

export const fetchPost = createServerFn({ method: 'GET' })
  .validator(z.object({ postId: z.string() }))
  .handler(async ({ data }) => {
    const post = await db.query.posts.findFirst({
      where: (posts, { eq }) => eq(posts.id, data.postId),
      with: { author: true }
    })
    if (!post) throw new Error('Post not found')
    return post
  })

export const createPost = createServerFn({ method: 'POST' })
  .middleware([requireAuth])
  .validator(z.object({ title: z.string().min(1), content: z.string().min(1) }))
  .handler(async ({ data, context }) => {
    return db.insert(posts).values({ ...data, authorId: context.user.id }).returning()
  })
Enter fullscreen mode Exit fullscreen mode

Using in a component:

function NewPostPage() {
  const navigate = useNavigate()
  const create = useServerFn(createPost)

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const data = new FormData(e.currentTarget)
    const post = await create({
      data: { title: data.get('title') as string, content: data.get('content') as string }
    })
    navigate({ to: '/posts/$postId', params: { postId: post.id } })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Publish</button>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Typed Search Params

const searchSchema = z.object({
  page: z.number().default(1),
  tag: z.string().optional(),
  sort: z.enum(['newest', 'popular']).default('newest'),
})

export const Route = createFileRoute('/posts/')({
  validateSearch: searchSchema,
  loader: async ({ search }) => {
    // search.page, search.tag, search.sort — all typed
    return fetchPosts({ data: { ...search } })
  },
  component: PostsList,
})
Enter fullscreen mode Exit fullscreen mode

Contrast with Next.js useSearchParams() which returns URLSearchParams — strings only, no validation.

Streaming SSR

export const Route = createFileRoute('/dashboard')({
  loader: async () => {
    const [stats, activityPromise] = await Promise.all([
      fetchDashboardStats({ data: {} }),
      defer(fetchRecentActivity({ data: {} }))  // streams later
    ])
    return { stats, activityPromise }
  },
  component: Dashboard,
})

function Dashboard() {
  const { stats, activityPromise } = Route.useLoaderData()

  return (
    <div>
      <StatsPanel stats={stats} />  {/* Immediate */}
      <Suspense fallback={<ActivitySkeleton />}>
        <Await promise={activityPromise}>
          {(activity) => <ActivityFeed activity={activity} />}
        </Await>
      </Suspense>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Deployment

// app.config.ts
export default defineConfig({
  server: {
    preset: 'vercel',  // or 'cloudflare-pages', 'aws-lambda', 'bun', 'node-server'
  }
})
Enter fullscreen mode Exit fullscreen mode

No vendor lock-in — switching deployment targets is a config change.

TanStack Start vs Next.js

TanStack Start Next.js App Router
Route param types End-to-end typed Promise<{}> — partial
Search param types Zod schema, automatic Manual casting
Server data fetching Server functions Server Components + Actions
Vendor lock-in None (Nitro) Vercel-optimized
Deploy targets Anywhere Nitro runs Vercel-optimized
Ecosystem maturity Growing (2026 stable) Mature

When TanStack Start wins:

  • Type safety is the top priority
  • Deploying to Cloudflare Workers or non-Vercel infrastructure
  • Already using TanStack Router and want the full-stack story
  • Want server-side data fetching without the React Server Components mental model

When Next.js is still right:

  • Heavy use of next/image, next/font, ISR, Vercel-specific features
  • Large existing codebase — not worth migrating
  • Team knows Next.js deeply

For new projects where type safety is a first-class requirement and you don't want to be tied to Vercel's infrastructure, TanStack Start is the serious alternative Next.js finally has.


Full article at stacknotice.com/blog/tanstack-start-complete-guide-2026

Top comments (0)