TanStack Start skill for Codex

Server functions that validate, loaders that stay portable. Codex checks the installed Start version against known API renames before writing code, then keeps database and secret access inside validated server functions guarded by middleware. Loaders call those functions so client navigations work, webhooks become server routes with signature checks, and the build output is grepped to confirm server-only code never reaches the browser.

  • Frontend
  • $tanstack-start
  • 205 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- tanstack-start

Installs to ~/.agents/skills/tanstack-start/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $tanstack-start, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds full-stack React apps on TanStack Start — Vite plugin setup, the __root document, server functions with createServerFn and input validation, request and function middleware, server routes for HTTP endpoints, isomorphic loaders that call server functions, selective SSR, environment variables, and hosting targets. Use when creating or extending a TanStack Start app, writing server functions or API endpoints, when secrets or database code leak into the client bundle, when a loader fails on client navigation, or when upgrading Start across beta and RC API renames.

Use it when

  • Creating or extending a TanStack Start application
  • Writing server functions, middleware or HTTP endpoints in Start
  • Server-only code or secrets appear in the client bundle
  • Upgrading across Start beta and RC API changes

Use something else when

  • Only client-side routing and search params are involved: use tanstack-router
  • Caching and invalidation of fetched data on the client: use tanstack-query

How to check the result

The build passes with no server-only identifiers in client output, and protected server functions reject calls without a session or with invalid input.

The mistake it prevents

Querying the database directly inside a route loader, which works on first load but runs in the browser on client navigation.

~/.agents/skills/tanstack-start/SKILL.md

--- name: tanstack-start description: Builds full-stack React apps on TanStack Start — Vite plugin setup, the __root document, server functions with createServerFn and input validation,… license: MIT ---

Done well means: server-only code runs only inside server functions, server routes or server-only helpers; loaders stay isomorphic and reach the server through server functions; every server function validates its input and checks auth in middleware; and the code uses the API names of the installed Start version, not a blog post’s.

Know the version first

Start moved fast through beta and RC. Before writing code, read package.json (@tanstack/react-start, @tanstack/react-router) and match these renames:

Older codeCurrent code
@tanstack/start, Vinxi, app.config.ts@tanstack/react-start, Vite plugin in vite.config.ts
.validator(...) on createServerFn.inputValidator(...)
createAPIFileRoute / createServerFileRouteserver: { handlers: { GET, POST } } on createFileRoute
export function createRouter() in router.tsxexport function getRouter()
serverOnly(fn) / clientOnly(fn)createServerOnlyFn(fn) / createClientOnlyFn(fn)

If the project is on an older line, either keep its idioms consistently or upgrade in a dedicated change. Upgrading needs network access to install packages; ask for approval when the sandbox blocks it. Read AGENTS.md for the package manager and scripts.

Project skeleton

// vite.config.ts
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [tanstackStart(), viteReact()], // Start's plugin must come before React's
});
// src/routes/__root.tsx
import { createRootRouteWithContext, HeadContent, Outlet, Scripts } from "@tanstack/react-router";

export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
  head: () => ({ meta: [{ charSet: "utf-8" }, { title: "Acme" }] }),
  component: () => (
    <html lang="en">
      <head><HeadContent /></head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  ),
});

src/router.tsx exports getRouter(), which creates the router with routeTree and context. Routing itself (search params, loaders, links) follows the tanstack-router skill.

Server functions

// src/features/projects/server.ts
import { createServerFn } from "@tanstack/react-start";
import { notFound } from "@tanstack/react-router";
import { z } from "zod";
import { authMiddleware } from "@/server/auth-middleware";
import { db } from "@/server/db";

export const getProject = createServerFn({ method: "GET" })
  .middleware([authMiddleware])
  .inputValidator(z.object({ id: z.string().uuid() }))
  .handler(async ({ data, context }) => {
    const project = await db.project.findFirst({ where: { id: data.id, orgId: context.user.orgId } });
    if (!project) throw notFound();
    return { id: project.id, name: project.name, updatedAt: project.updatedAt.toISOString() };
  });

export const renameProject = createServerFn({ method: "POST" })
  .middleware([authMiddleware])
  .inputValidator(z.object({ id: z.string().uuid(), name: z.string().trim().min(1).max(80) }))
  .handler(async ({ data, context }) => {
    await db.project.updateMany({ where: { id: data.id, orgId: context.user.orgId }, data: { name: data.name } });
    return { ok: true as const };
  });
  • Call with { data }: await getProject({ data: { id } }).
  • GET for reads (cacheable, idempotent), POST for writes.
  • A server function is a public HTTP endpoint. Validation and auth are not optional.
  • Return plain serializable data. Map ORM rows to DTOs; never return the full record.
  • Import server functions statically. Dynamic import() of a server function module defeats the compiler’s server/client split.

Loaders are isomorphic

Route loaders run on the server for the first request and in the browser on client navigations. Code inside a loader therefore ships to the client.

export const Route = createFileRoute("/projects/$projectId")({
  loader: ({ params }) => getProject({ data: { id: params.projectId } }), // RPC on client nav
  component: ProjectPage,
});
  • Never query the database or read secrets directly in a loader; call a server function.
  • With TanStack Query: loader: ({ context, params }) => context.queryClient.ensureQueryData(projectQuery(params.projectId)), where the query’s queryFn calls the server function.
  • For code that must differ by environment, use createIsomorphicFn().server(...).client(...).

Middleware

// src/server/auth-middleware.ts
import { createMiddleware } from "@tanstack/react-start";
import { getRequest } from "@tanstack/react-start/server";

export const authMiddleware = createMiddleware({ type: "function" }).server(async ({ next }) => {
  const user = await getUserFromSession(getRequest());
  if (!user) throw new Error("Unauthorized"); // or throw redirect({ to: "/login" })
  return next({ context: { user } });
});
TypeRuns forUse for
createMiddleware({ type: "function" })server function callsauth, input logging, adding context; .client() can add headers
createMiddleware() (request, default)every server request, including SSR and server routessecurity headers, request logging, rate limiting

Register app-wide middleware in src/start.ts with createStart(() => ({ requestMiddleware: [...], functionMiddleware: [...] })).

Server routes (HTTP endpoints)

// src/routes/api/webhooks/billing.ts
export const Route = createFileRoute("/api/webhooks/billing")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const raw = await request.text();
        if (!verifySignature(raw, request.headers.get("x-signature"))) {
          return new Response("invalid signature", { status: 401 });
        }
        await enqueueBillingEvent(JSON.parse(raw));
        return new Response(null, { status: 204 });
      },
    },
  },
});

Use server routes for webhooks, third-party callers, file downloads and anything not called from your own UI. Use server functions for your UI’s reads and writes.

Rendering and environment

  • Selective SSR per route: ssr: true (default), "data-only" (run loader on server, render component on client) or false (client only, for browser-dependent widgets).
  • Environment variables: only VITE_-prefixed values are exposed via import.meta.env in client code. Read secrets with process.env inside server functions, server routes or createServerOnlyFn helpers.
  • Hosting: the Vite build targets Node by default; providers such as Netlify, Vercel and Cloudflare have dedicated plugins or presets. Follow the hosting guide for the installed version rather than copying config from another project.

Gotchas

  • A helper that imports the DB client, used by both a server function and a component, drags the DB client toward the client bundle. Keep server modules under src/server/ and import them only from server functions and server routes.
  • Server function return values must be serializable; Date and other rich types depend on the serializer of your version. Return ISO strings when in doubt.
  • Cookie-authenticated POST server functions need CSRF protection: SameSite=Lax (or stricter) session cookies plus an origin check in request middleware.
  • Throwing a plain Error in a server function surfaces as an error on the client; for expected failures, return a typed result instead.
  • After renaming a route file, the generated routeTree.gen.ts updates on the next dev or build run; type errors before that are stale.

Verification

  • vite build passes; grep the client output for server-only identifiers (grep -r "DATABASE_URL\|prisma" dist/client .output/public 2>/dev/null adjusted to the build output folder) and expect no matches.
  • tsc --noEmit passes.
  • Load each touched route directly (SSR) and through client navigation; both paths return the same data.
  • Call a protected server function without a session (from tests or curl against the dev server) and confirm rejection; send invalid input and confirm a validation error.

Output

List server functions (method, input schema, middleware, returned shape), server routes with their auth or signature checks, and routes with non-default SSR modes.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "TanStack Start"
  short_description: "Server functions that validate, loaders that stay portable"
  default_prompt: "$tanstack-start Add a getProject server function with Zod validation and an auth middleware"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem