
Tanstack Start Best Practices
Ship TanStack Start apps with correct server routes for webhooks, REST APIs, and external HTTP consumers instead of misusing server functions.
Overview
TanStack Start Best Practices is an agent skill for the Build phase that teaches when and how to create TanStack Start server routes for webhooks, REST endpoints, and external HTTP clients.
Install
npx skills add https://github.com/deckardger/tanstack-agent-skills --skill tanstack-start-best-practicesWhat is this skill?
- Contrasts server functions (internal RPC) vs server routes (REST, webhooks, external consumers)
- Shows createFileRoute server handlers with GET/POST and json() responses plus Cache-Control
- Calls out webhook pitfalls: signature verification needs raw body access
- Documents MEDIUM-priority api-routes guidance within TanStack Start best-practices pack
- Steers versioning and standard HTTP semantics away from exposing internal createServerFn endpoints
Adoption & trust: 6.7k installs on skills.sh; 180 GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You are on TanStack Start but reach for server functions everywhere, which breaks webhooks, caching, and standard API contracts external tools expect.
Who is it for?
Full-stack solo builders shipping TanStack Start who need public APIs or webhook endpoints alongside React UI.
Skip if: Teams on Next.js-only stacks, static SPAs with no TanStack Start server layer, or apps with zero external HTTP consumers.
When should I use this skill?
User is implementing TanStack Start server HTTP surface, webhooks, or public REST and needs best-practice route patterns.
What do I get? / Deliverables
You add file-based API routes with correct HTTP semantics and keep server functions scoped to internal RPC, ready to wire Stripe or partner integrations.
- Server route modules under routes/api with GET/POST handlers
- Documented split between internal RPC and external HTTP endpoints
Recommended Skills
Journey fit
Canonical shelf is Build because the skill encodes routing and handler patterns while implementing the product’s server surface. Backend subphase fits API routes, raw request bodies, cache headers, and third-party webhook semantics that server functions do not cover well.
How it compares
Framework-native route handlers for external HTTP—not a generic OpenAPI generator or separate Express sidecar.
Common Questions / FAQ
Who is tanstack-start-best-practices for?
Indie full-stack developers using TanStack React Start who want agent-guided conventions for server routes, caching, and integration endpoints.
When should I use tanstack-start-best-practices?
During Build when adding /api routes, fixing webhook handlers, or separating internal server functions from REST; also before Ship when hardening partner-facing endpoints.
Is tanstack-start-best-practices safe to install?
Review the Security Audits panel on this Prism page and inspect the skill repo; the skill describes code patterns and does not require cloud credentials by itself.
SKILL.md
READMESKILL.md - Tanstack Start Best Practices
# api-routes: Create Server Routes for External Consumers ## Priority: MEDIUM ## Explanation While server functions are ideal for internal RPC, server routes provide traditional REST endpoints for external consumers, webhooks, and integrations. Use server routes when you need standard HTTP semantics, custom response formats, or third-party compatibility. ## Bad Example ```tsx // Using server functions for webhook endpoints export const stripeWebhook = createServerFn({ method: 'POST' }) .handler(async ({ request }) => { // Server functions aren't designed for raw request handling // No easy access to raw body for signature verification // Response format is JSON by default }) // Or exposing internal functions to external consumers export const getUsers = createServerFn() .handler(async () => { return db.users.findMany() }) // No versioning, no standard REST semantics ``` ## Good Example: Basic Server Route ```tsx // routes/api/users.ts import { createFileRoute } from '@tanstack/react-router' import { json } from '@tanstack/react-start' export const Route = createFileRoute('/api/users')({ server: { handlers: { GET: async ({ request }) => { const users = await db.users.findMany({ select: { id: true, name: true, email: true }, }) return json(users, { headers: { 'Cache-Control': 'public, max-age=60', }, }) }, POST: async ({ request }) => { const body = await request.json() // Validate input const parsed = createUserSchema.safeParse(body) if (!parsed.success) { return json({ error: parsed.error.flatten() }, { status: 400 }) } const user = await db.users.create({ data: parsed.data }) return json(user, { status: 201 }) }, }, }, }) ``` ## Good Example: Webhook Handler ```tsx // routes/api/webhooks/stripe.ts import { createFileRoute } from '@tanstack/react-router' import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) export const Route = createFileRoute('/api/webhooks/stripe')({ server: { handlers: { POST: async ({ request }) => { const signature = request.headers.get('stripe-signature') if (!signature) { return new Response('Missing signature', { status: 400 }) } // Get raw body for signature verification const rawBody = await request.text() let event: Stripe.Event try { event = stripe.webhooks.constructEvent( rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET! ) } catch (err) { console.error('Webhook signature verification failed:', err) return new Response('Invalid signature', { status: 400 }) } // Handle the event switch (event.type) { case 'checkout.session.completed': await handleCheckoutComplete(event.data.object) break case 'customer.subscription.updated': await handleSubscriptionUpdate(event.data.object) break default: console.log(`Unhandled event type: ${event.type}`) } return new Response('OK', { status: 200 }) }, }, }, }) ``` ## Good Example: RESTful Resource with Dynamic Params ```tsx // routes/api/posts/$postId.ts import { createFileRoute } from '@tanstack/react-router' import { json } from '@tanstack/react-start' export const Route = createFileRoute('/api/posts/$postId')({ server: { handlers: { GET: async ({ params }) => { const post = await db.posts.findUnique({ where: { id: params.postId }, }) if (!post) { return json({ error: 'Post not found' }, { status: 404 }) } return json(post) }, PUT: async ({ request, params }) => { const body = await request.json() const pars