Tanstack start project structure
Agent skills library for AI coding assistants. Includes coding conventions, npm package preferences, and project bootstrapping tools.
npx -y skills add r-portas/skills --skill tanstack-start-project-structureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
File layout and code-organization conventions for Roy's TanStack Start projects — where files live under `src/`, the `.functions.ts` / `.server.ts` / `.schema.ts` split for business logic, route and loader structure, and how the client/server boundary is enforced. Consult when creating any file under `src/`, adding or editing a route or loader, writing or modifying a `createServerFn` server function, or deciding where a piece of logic should live.
SKILL.md
13.4 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it
Project Structure
Directory layout
| Path | What goes there |
|---|---|
src/router.tsx | Router factory (getRouter()). Imports routeTree.gen.ts and creates the router instance. |
src/routes/ | TanStack Start file-based routes |
src/components/ui/ | Base UI primitives (shadcn/ui conventions) |
src/components/<domain>/ | Domain-specific component groupings (e.g. home/, posts/) |
src/components/ | Shared components used across multiple routes |
src/hooks/ | Custom React hooks |
src/lib/ | Non-UI application operations, server-only helpers, schemas, and utilities |
src/db/ | Database client and schema (Drizzle) |
src/global.css | shadcn theme variables and global CSS |
routeTree.gen.ts | Auto-generated route tree — do not edit by hand. |
src/lib/
Non-UI code lives in src/lib/: server functions, business workflows, validation schemas, server-only helpers, and isomorphic utilities. Keep the layout flat — no folders per domain. For larger applications, group capabilities with domain-prefixed files like posts.functions.ts, comments.server.ts, and authors.schema.ts. Files are grouped by suffix:
| Suffix | Purpose | Importable from |
|---|---|---|
.functions.ts | createServerFn exports. Application operations live in handler bodies — this is the service layer. Handlers should represent meaningful user or business actions like publishPost, createComment, or updateAuthorProfile, not thin CRUD wrappers. The compiler rewrites handlers into RPC stubs for client bundles, so this file is safe to import from anywhere. | Anywhere (client and server) |
.server.ts | Optional. Server-only helpers, policy checks, workflow helpers, integration code, constants, background jobs, or cross-domain primitives consumed by .functions.ts files. TanStack Start treats .server.ts as server-only and rejects client imports. | Other .functions.ts and .server.ts files (including across domains). Never from routes or components. |
.schema.ts | Zod schemas and their inferred types (z.infer<...>). Singular by convention. | Anywhere |
.test.ts | Unit tests, matched to source: apps.functions.test.ts, apps.server.test.ts. | Test runner only |
.ts (no suffix) | Isomorphic helpers — client-safe, framework-agnostic (utils.ts, date.ts, format.ts). | Anywhere |
Core rules
.functions.tsis the application/service layer. EachcreateServerFnhandler should own a meaningful operation: validate input, authorize the caller, orchestrate domain rules, read/write data, and call server-only integrations as needed. Do not create separateservices/,use-cases/, orrepositories/layers unless the existing project already has that convention. Avoid thin wrappers around single queries..server.tsis optional. Create it only when a domain has server-only helpers worth naming: policy checks, workflow helpers, shared queries, integration code, constants, or background jobs (e.g.assertCanPublishPost,notifySubscribers,renderMarkdown). If the logic is only used once and reads clearly inside the.functions.tshandler, keep it inline. Don't create empty files for symmetry..server.tsexports are cross-domain.posts.functions.tsmay importfindAuthorBySlugfromauthors.server.ts;comments.functions.tsmay importgetPostCommentPolicyfromposts.server.ts. This avoids wrapping internal helpers in unnecessarycreateServerFncalls.- Types live where they're produced. Zod-inferred types stay next to their schemas in
.schema.ts. Function return-shape interfaces live in the.functions.tsthat returns them. Domain unions live in the file that owns the concept. No dedicated.types.tsfile. - Routes compose in loaders. Each
createServerFninsrc/lib/takes one input and returns one well-defined shape; route loaders call these functions, often in parallel viaPromise.all, to assemble view data. - Server functions are reusable application operations. Name them after the operation or data they provide, and keep them useful outside a single route:
getPost,listPosts,publishPost,createComment,approveComment,getAuthorProfile. Do not create server functions that exist only to return everything one route needs.
CRITICAL: Never import .server.ts files from src/routes/ or src/components/. TanStack Start rejects the import at build time, but the error trace can be hard to read. Enforce it with a no-restricted-imports lint rule so failures surface immediately, not at build.
CRITICAL: In .functions.ts, only use server-only resources inside handler bodies. The compiler strips createServerFn handler bodies and replaces them with RPC stubs on the client — but other module-level code in the same file ships to the client. Reading process.env, importing db, calling fs / shell, etc. at module scope in a .functions.ts leaks server code (and secrets) into the client bundle. Keep that work inside handler bodies, or move it to .server.ts and call it from a handler.
For files that must be server-only but don't use the .server.ts naming convention, add this marker at the top:
import '@tanstack/react-start/server-only'
Example layout
src/lib/
├── posts.functions.ts # getPost, listPosts, publishPost, archivePost
├── posts.server.ts # notifySubscribers, renderMarkdown, findPostBySlug
├── posts.schema.ts # post input schemas + inferred types
├── posts.functions.test.ts
│
├── authors.functions.ts # getAuthorProfile, updateAuthorProfile
├── authors.server.ts # findAuthorBySlug — used by posts.functions.ts too
├── authors.schema.ts
│
├── comments.functions.ts # listCommentsForPost, createComment, approveComment
├── comments.server.ts # getPostCommentPolicy, notifyCommentModerators
├── comments.schema.ts
│
├── tags.functions.ts # listTags, updatePostTags
├── tags.schema.ts # no .server.ts — nothing to put there
│
├── markdown.server.ts # infra primitives, no domain
├── email.server.ts # infra; notification sending
├── logger.ts # isomorphic logging facade
│
├── format.ts # isomorphic display formatters
├── date.ts # isomorphic
└── utils.ts # isomorphic (cn)
src/routes/
TanStack Start uses file-based routing. Each file maps to a URL segment. Use folder-per-param when a dynamic segment has children; leave it flat when it's a leaf:
src/routes/
├── __root.tsx # Root layout (nav, providers, etc.)
├── index.tsx # /
├── about.tsx # /about — leaf route, flat
└── posts/
├── index.tsx # /posts
└── $postId/
├── index.tsx # /posts/:postId
└── edit.tsx # /posts/:postId/edit
Each route file exports a single Route constant from createFileRoute, plus an unexported component function:
export const Route = createFileRoute("/posts/$postId/")({
loader: ({ params }) => { ... },
component: PostDetail,
});
function PostDetail() { // unexported — preserves code splitting
const data = Route.useLoaderData();
const { postId } = Route.useParams();
// ...
}
CRITICAL: Never export component functions from route files. Exported functions are included in the main bundle and bypass code splitting entirely. Keep component functions unexported.
CRITICAL: Loaders are isomorphic — they run on both server and client. Never put DB queries, secrets, or Node-only code directly in a loader. Always wrap server-only logic in createServerFn and call it from the loader.
Loader composition
Compose data from multiple primitive createServerFns in src/lib/ via Promise.all:
export const Route = createFileRoute("/authors/$authorSlug/")({
loader: async ({ params }) => {
const author = await getAuthor({ data: { slug: params.authorSlug } });
if (!author) throw notFound();
const [posts, recentComments] = await Promise.all([
listPostsByAuthor({ data: { authorSlug: params.authorSlug } }),
listRecentComments({ data: { authorSlug: params.authorSlug } }),
]);
return { author, posts, recentComments };
},
component: AuthorPage,
});
src/db/
Two files live directly under src/db/:
index.ts— the Drizzle client, imported wherever queries are runschema.ts— all table definitions
src/db/
├── index.ts # export const db = drizzle(...)
└── schema.ts # export const postsTable = sqliteTable(...)
CRITICAL: Mark db/index.ts as server-only. Add the server-only import at the top so any client-side import becomes a build error instead of a silent leak of the Drizzle client (and your DB connection) into the client bundle:
// src/db/index.ts
import "@tanstack/react-start/server-only";
import { drizzle } from "drizzle-orm/...";
// ...
export const db = drizzle(...);
db is then safe to use inside createServerFn handler bodies and .server.ts files. Routes, components, and any client-only file that tries to import @/db will fail the build immediately.
Importing types from schema.ts (e.g. typeof postsTable.$inferSelect) is safe everywhere — types are stripped at compile time. Importing the table values (postsTable itself) is server-only, so schema.ts typically does not get the server-only marker — that would block type imports too.
See the bootstrap skill for the full Drizzle setup.
src/global.css
Contains shadcn/ui CSS variable definitions (the theme) and any global styles. Don't add component-specific styles here — those belong in Tailwind classes on the component.