Courses/React + Vite + Go
Standalone Course~30 min12 challenges

React + Vite + Go: Building with TanStack Router

Not every app needs server-side rendering. In this course, you will learn how to use TanStack Router with Vite as your frontend instead of Next.js — perfect for dashboards, internal tools, and admin panels where SEO doesn't matter and speed is everything.


Why TanStack Router?

Throughout the Grit Web course, you used Next.js as your frontend. Next.js is great for public-facing websites that need SEO, but it comes with overhead you don't always need — server-side rendering, server components, hydration. For apps where every user is logged in, there's a faster alternative.

SPA (Single Page Application): A web application that loads a single HTML page and dynamically updates content as the user interacts with it. The browser downloads the entire app upfront, then all navigation happens client-side without full page reloads. Gmail, Figma, and Notion are SPAs.
Vite: A modern build tool for JavaScript/TypeScript projects. It serves code via native ES modules during development (instant startup, near-instant hot reload) and uses Rollup for optimized production builds. Much faster than webpack-based tools.
TanStack Router: A fully type-safe router for React applications built by Tanner Linsley (the creator of TanStack Query). It supports file-based routing (like Next.js), URL search params as state, and deep integration with TypeScript — every route, parameter, and link is type-checked at compile time.

Here's how TanStack Router + Vite compares to Next.js:

FeatureTanStack Router + ViteNext.js
RenderingClient-side only (SPA)Server + Client (SSR/SSG)
Dev startup~200ms (Vite HMR)~2-5s (compilation)
Bundle sizeSmaller (no SSR runtime)Larger (includes SSR)
SEOPoor (client-rendered)Excellent (pre-rendered)
Type safetyFull (routes, params, links)Partial
Best forDashboards, admin panels, internal toolsPublic websites, blogs, e-commerce
If your app requires login to use (dashboard, admin panel, CRM), TanStack Router + Vite is likely the better choice. If your app has public pages that need to rank on Google (blog, landing page, product listings), stick with Next.js.
1

Challenge: When Would You Choose TanStack Router?

For each of these projects, decide whether you'd use TanStack Router + Vite or Next.js, and explain why: (1) a company blog, (2) an employee timesheet tracker, (3) a real estate listing site, (4) a project management dashboard.

Scaffold with Vite

Grit supports Vite as an alternative frontend. When you pass the --vite flag, Grit scaffolds TanStack Router instead of Next.js:

Terminal
grit new dashboard --double --vite

This creates a Double architecture (Go API + frontend) but with a Vite-powered React app instead of Next.js. Here's what's different:

Next.js (--next)TanStack Router (--vite)
app/ directorysrc/routes/ directory
next.config.jsvite.config.ts
layout.tsx for layouts__root.tsx for root layout
page.tsx for pagesindex.tsx or about.tsx for pages
2

Challenge: Scaffold and Compare

Scaffold two projects — one with --next and one with --vite. Compare the frontend folder structures. How many files are different? Which has more configuration files?

File-Based Routing

TanStack Router discovers routes from your file system, just like Next.js. But the naming conventions are different:

File-Based Routing: The file and folder structure inside your routes directory determines your URL routes. No manual route configuration needed. TanStack Router reads the file tree, auto-generates a route tree, and provides full type safety for every route. Drop a file in the right folder and it becomes a URL.
src/routes/
src/routes/
├── __root.tsx       ← Root layout (navbar, providers)
├── index.tsx        ← / (home page)
├── about.tsx        ← /about
├── products/
│   ├── index.tsx    ← /products (list)
│   ├── $id.tsx      ← /products/:id (detail)
│   └── new.tsx      ← /products/new (create)

Key naming rules:

  • __root.tsx — the root layout that wraps every page (like Next.js' root layout.tsx)
  • index.tsx — the default page for a directory (like Next.js' page.tsx)
  • $param.tsx — a dynamic route segment (Next.js uses [param])
  • _layout.tsx — a nested layout for a route group
TanStack Router auto-generates a routeTree.gen.ts file every time you add or remove a route file. This file contains the full type-safe route tree. Never edit it manually — it's regenerated automatically.
3

Challenge: Create a New Route

Create a file at src/routes/settings.tsx with a simple component. Run the dev server — does TanStack Router pick it up automatically? Can you navigate to /settings?

Route Parameters

Files starting with $ create dynamic route parameters. The $id.tsx file matches any value in that URL segment and makes it available as a typed parameter:

src/routes/products/$id.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/products/$id')({
  component: ProductDetail,
})

function ProductDetail() {
  const { id } = Route.useParams()

  return (
    <div>
      <h1>Product {id}</h1>
      {/* Fetch and display product details */}
    </div>
  )
}

The id parameter is fully type-safe. If you try to access a parameter that doesn't exist, TypeScript will catch the error at compile time. This is a major advantage over Next.js' params prop, which is loosely typed.

You can have multiple parameters in a single route. A file at src/routes/teams/$teamId/members/$memberId.tsx gives you both teamId and memberId as typed params.
4

Challenge: Create a Dynamic Route

Create a route at src/routes/users/$id.tsx that displays the user ID from the URL. Navigate to /users/42 and /users/hello — does the parameter work for both?

Layouts

The __root.tsx file is the root layout — it wraps every page in your app. This is where you place your navbar, providers (query client, auth context), and global styles:

src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { Navbar } from '../components/navbar'

export const Route = createRootRoute({
  component: RootLayout,
})

function RootLayout() {
  return (
    <div className="min-h-screen bg-background">
      <Navbar />
      <Outlet />
    </div>
  )
}

The Outlet component renders the current route's page — just like the children prop in Next.js layouts.

For nested layouts (e.g., a sidebar that only appears on dashboard pages), create a route group with a layout file:

src/routes/dashboard/
src/routes/
├── dashboard/
│   ├── _layout.tsx      ← Sidebar layout for all /dashboard/* routes
│   ├── index.tsx        ← /dashboard (main dashboard)
│   ├── projects.tsx     ← /dashboard/projects
│   └── settings.tsx     ← /dashboard/settings
5

Challenge: Add a Sidebar Layout

Create a dashboard/_layout.tsx that renders a sidebar on the left and the page content on the right. Add at least 3 routes under /dashboard/. Does the sidebar persist across navigation?

Data Fetching

Data fetching in a Vite app is identical to what you learned in the Grit Web course. TanStack Query (useQuery, useMutation) works the same way — it's a React library, not a Next.js feature. The only difference is that everything runs client-side.

src/routes/products/index.tsx
import { useQuery } from '@tanstack/react-query'
import { api } from '../../lib/api-client'

function ProductList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['products'],
    queryFn: () => api.get('/api/products'),
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error loading products</div>

  return (
    <div>
      <h1>Products</h1>
      {data.data.map((product: any) => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  )
}
Since there's no server-side rendering, the user sees a loading state while data is being fetched. This is normal for SPAs. For dashboards and internal tools, this is perfectly acceptable. For public pages where you want content visible immediately, you'd use Next.js with SSR.
6

Challenge: Fetch and Display Data

Generate a Product resource with grit generate. Then build a products list page that fetches from /api/products and displays them in a table. Does the loading state appear before the data?

Navigation

TanStack Router provides its own Link component and useNavigate hook. The Link component is type-safe — it only allows you to link to routes that actually exist:

Navigation Examples
import { Link, useNavigate } from '@tanstack/react-router'

// Declarative navigation (in JSX)
<Link to="/products">All Products</Link>

// With parameters — fully type-checked
<Link to="/products/$id" params={{ id: '42' }}>
  View Product
</Link>

// Programmatic navigation (in event handlers)
function CreateButton() {
  const navigate = useNavigate()

  const handleCreate = async () => {
    const product = await createProduct(data)
    navigate({ to: '/products/$id', params: { id: product.id } })
  }

  return <button onClick={handleCreate}>Create</button>
}

If you try to link to a route that doesn't exist, TypeScript shows an error immediately. If you forget a required parameter, TypeScript catches that too. This is a significant improvement over Next.js' Link component, which accepts any string.

7

Challenge: Add Navigation Links

Add a navbar with Link components to your root layout. Include links to Home, Products, and Dashboard. Then add a button that uses useNavigate to redirect to a product detail page after creating a product.

Auth in Vite Apps

Authentication in a Vite SPA works the same way as in Next.js, with one key difference: there's no server to manage cookies. Instead, you store the JWT token in localStorage and include it in every API request via an Authorization header.

src/lib/auth-context.tsx
// Auth context wraps the entire app
function AuthProvider({ children }) {
  const [token, setToken] = useState(localStorage.getItem('token'))
  const [user, setUser] = useState(null)

  const login = async (email: string, password: string) => {
    const res = await api.post('/api/auth/login', { email, password })
    localStorage.setItem('token', res.data.token)
    setToken(res.data.token)
    setUser(res.data.user)
  }

  const logout = () => {
    localStorage.removeItem('token')
    setToken(null)
    setUser(null)
  }

  return (
    <AuthContext.Provider value={{ user, token, login, logout }}>
      {children}
    </AuthContext.Provider>
  )
}

To protect routes, check for the token and redirect to /login if not authenticated. You can do this in a layout component that wraps all protected routes:

src/routes/dashboard/_layout.tsx
function DashboardLayout() {
  const { user } = useAuth()
  const navigate = useNavigate()

  if (!user) {
    navigate({ to: '/login' })
    return null
  }

  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1 p-6">
        <Outlet />
      </main>
    </div>
  )
}
8

Challenge: Implement a Protected Route

Set up an auth context with login/logout. Create a /login page and a protected /dashboard layout. Verify that visiting /dashboard without logging in redirects to /login.

Building for Production

When you're ready to deploy, build the Vite frontend into static files:

Terminal
cd frontend && pnpm build

This outputs optimized files to frontend/dist/. Vite tree-shakes unused code, minifies everything, and splits the bundle into chunks for optimal loading.

For Grit's Single architecture, the dist/ folder gets embedded directly into the Go binary via go:embed. This means your entire app — API and frontend — is a single executable file:

cmd/server/main.go
//go:embed all:frontend/dist
var frontendFS embed.FS

// Serve frontend from the embedded files
router.NoRoute(gin.WrapH(http.FileServer(http.FS(frontendFS))))
A typical Vite + TanStack Router build produces a bundle between 150-300 KB gzipped, compared to 500 KB+ for a Next.js app. The smaller bundle means faster initial load for your users.
9

Challenge: Build and Measure

Run pnpm build in the frontend directory. Check the output — what's the total bundle size? How many chunks were created? Compare this to a Next.js build if you have one available.

Vite Dev Server Proxy

During development, the Vite dev server runs on one port (typically 5173) and the Go API runs on another (typically 8080). The Vite proxy configuration forwards API requests to the Go server so you don't have to deal with CORS:

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [TanStackRouterVite(), react()],
  server: {
    proxy: {
      '/api': 'http://localhost:8080',
    },
  },
})

With this configuration, when your frontend code calls fetch("/api/products"), Vite intercepts the request and forwards it to http://localhost:8080/api/products. This only applies during development — in production, both frontend and API are served from the same origin.

10

Challenge: Explore the Proxy Config

Open vite.config.ts in your scaffolded project. Find the proxy configuration. What path prefix does it intercept? What happens if you change the Go server port — what else would you need to update?

When to Use Next.js vs Vite

Here's the decision framework:

CriteriaUse Next.jsUse Vite + TanStack Router
SEO needed?Yes — blog, e-commerce, landing pagesNo — dashboards, admin panels
Public pages?Many public pagesMostly behind login
Build speedSlower (webpack/turbopack)Faster (Vite/esbuild)
Bundle sizeLargerSmaller
Deploy toVercel, VPS, DockerEmbedded in Go binary, VPS, CDN
Type safetyPartial (loose route params)Full (routes, params, links)
11

Challenge: Make the Right Choice

For each project below, decide Next.js or Vite + TanStack Router, and explain your reasoning: (1) a personal blog with comments, (2) a company HR dashboard, (3) an online store with 10,000 products, (4) an internal inventory management tool, (5) a SaaS landing page + app.

What You Learned

  • When to choose TanStack Router + Vite over Next.js
  • How to scaffold a Vite project with grit new --vite
  • File-based routing with TanStack Router
  • Type-safe route parameters, layouts, and navigation
  • Data fetching with TanStack Query (same as Next.js)
  • JWT auth with localStorage for SPAs
  • Vite dev server proxy for API calls
  • Building for production and embedding in Go
12

Challenge: Build a Task Dashboard

Build a complete task dashboard from scratch:

  1. Scaffold with grit new task-dash --double --vite
  2. Generate a Task resource: grit generate resource Task title:string status:string priority:int due_date:date:optional
  3. Build a task list page with filtering by status
  4. Build a create/edit task form
  5. Add an auth guard on all routes
  6. Build and check the production bundle size