Skip to content

Loa212/turbo-tanstack-starter

Repository files navigation

turbo-tanstack-starter

A reusable, project-agnostic Turborepo + pnpm template for building TanStack single-page apps with an owned component library and Storybook.

It is scaffolding, not a product: the app is intentionally thin, there are no feature screens, no auth, and no real data fetching. Clone it, swap the @acme namespace and myapp name, swap the design tokens, and start building.


Workspace layout

turbo-tanstack-starter/
├── apps/
│   └── myapp/            @acme/myapp  — TanStack Router + Query SPA (Vite)
├── packages/
│   ├── ui/               @acme/ui     — owned component library (source-only) + Storybook
│   ├── theme/            @acme/theme  — design tokens (single source of truth)
│   └── config/           @acme/config — shared base tsconfig + Biome config
├── turbo.json            — task graph (build / dev / lint / storybook) + caching
├── pnpm-workspace.yaml   — workspace globs
└── package.json          — root scripts (everything runs through Turbo)
Package Name What it is
apps/myapp @acme/myapp The app. App chrome (Header), routes, TanStack Router/Query wiring.
packages/ui @acme/ui Hand-written, owned components (Button, Input, Table). Storybook here.
packages/theme @acme/theme theme.css — the shadcn CSS-variable token set (--background, --primary, --radius, …).
packages/config @acme/config tsconfig.base.json + biome/biome.base.json, extended by every package.

Philosophy

Owned components, not a black-box dependency

@acme/ui is source we control — shadcn-style components that are hand-written, composable, and live in the repo. They are not published to a registry and have no build step. The app consumes them through a workspace symlink:

// apps/myapp/package.json
"dependencies": { "@acme/ui": "workspace:*" }

Because it's a symlink to source, editing a component in packages/ui/src is reflected in the running app instantly — no rebuild, no version bump, no publish.

To add more primitives, run shadcn from the app — it's configured to install into the library, not the app:

cd apps/myapp
npx shadcn@latest add dialog   # lands in packages/ui/src/components

(apps/myapp/components.json points ui/utils/lib aliases at @acme/ui and css at @acme/theme/theme.css.)

Shared Tailwind theme + neutral tokens

Tailwind here is v4 — there is no tailwind.config.js. Theming is CSS-based, and the single source of truth is packages/theme/theme.css: it holds the token blocks (:root / .dark) and the @theme inline mapping. Both the app and Storybook @import it, so there is exactly one place that defines the design system.

Token values are intentionally neutral (the shadcn "zinc" default). Re-skinning a project is a token-value swap, not a refactor — edit the values in theme.css and the app and Storybook re-skin together.

Because @acme/ui ships raw source (no build), every Tailwind entry that renders its components must point Tailwind at the library so its classes are generated. That's the @source directive in apps/myapp/src/styles.css and packages/ui/.storybook/preview.css:

@import "tailwindcss";
@import "@acme/theme/theme.css";
@source "../../../packages/ui/src";   /* scan owned components */

Why TanStack Router + TanStack Query

  • TanStack Router — type-safe, file-based routing with first-class search params as state. Kept deliberately (not React Router): the type safety and URL-as-state model are the point.
  • TanStack Query — the server-state tool. It's the only state library here; no Redux/ Zustand/Jotai. Use Query for server state and Router search params for URL state.

SPA, not SSR

This is a static single-page app. The Nitro/SSR adapter from the original scaffold has been removed. pnpm build emits a plain static bundle deployable to any static host. (If a future project needs SSR, add an adapter then — the template stays SPA.)


Requirements

  • Node 20+ (this repo is pinned to Node 24 via .nvmrc; pnpm 11 itself requires Node ≥ 22.13)
  • pnpm (see packageManager in the root package.json)

Getting started

pnpm install
pnpm dev          # runs apps/myapp at http://localhost:3000

Scripts (all run through Turbo from the root)

Command What it does
pnpm dev Run apps/myapp (Vite dev server, port 3000).
pnpm build Build every package/app (Turbo, cached). App: vite build && tsc.
pnpm storybook Run Storybook for @acme/ui at http://localhost:6006.
pnpm build-storybook Build the static Storybook (packages/ui/storybook-static).
pnpm lint Biome lint across packages.
pnpm format Biome format (write) across the repo.
pnpm check Biome check --write across packages.
pnpm typecheck tsc --noEmit across packages.

You can scope any task to one package, e.g. pnpm --filter @acme/ui storybook.


Component library & Storybook

Storybook is configured against packages/ui and is the primary surface for developing and documenting components. It includes @storybook/addon-a11y so accessibility is a first-class concern in the dev loop.

Shipped components (each with stories):

  • Button — the prop-driven-variants example (variant × size via class-variance-authority, plus asChild polymorphism).
  • Input — text input with focus-ring and aria-invalid states.
  • Table — composable table shell (Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption).
pnpm storybook

Tooling

  • TypeScript strict everywhere via @acme/config/tsconfig.base.json (bundler resolution, verbatimModuleSyntax, no unused locals/params).
  • Biome only — formatting + linting (tabs, double quotes). No ESLint/Prettier. Base config in @acme/config/biome, extended per package.
  • Turborepo orchestrates tasks with dependsOn/caching; dev/storybook are persistent and uncached.

routeTree.gen.ts is generated by the TanStack Router Vite plugin on dev/build and is gitignored — never edit it by hand.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages

Generated from Loa212/my-tanstack-app