Back to list
oakoss

tanstack-start

by oakoss

Open-source SaaS starter kit with React, TanStack, and Better Auth

0🍴 0📅 Jan 26, 2026

SKILL.md


name: tanstack-start description: TanStack Start SSR framework. Use for server functions, API routes, SSR modes, middleware chains, server-only code

TanStack Start

Server Functions

import { createServerFn } from '@tanstack/react-start';
import { z } from 'zod';

// GET - No input validation needed
const getUsers = createServerFn({ method: 'GET' }).handler(async () => {
  return await db.query.users.findMany();
});

// POST - Always validate with Zod
const createUser = createServerFn({ method: 'POST' })
  .inputValidator(z.object({ name: z.string().min(1), email: z.email() }))
  .handler(async ({ data }) => {
    return await db.insert(users).values(data).returning();
  });

Authenticated Server Functions

import { auth } from '@oakoss/auth/server';

const deletePost = createServerFn({ method: 'POST' })
  .inputValidator(z.object({ id: z.string() }))
  .handler(async ({ data, request }) => {
    const session = await auth.api.getSession({ headers: request.headers });

    if (!session) {
      return { error: 'Authentication required', code: 'AUTH_REQUIRED' };
    }

    const post = await db.query.posts.findFirst({
      where: eq(posts.id, data.id),
    });

    if (post?.authorId !== session.user.id) {
      return { error: 'Not authorized', code: 'FORBIDDEN' };
    }

    await db.delete(posts).where(eq(posts.id, data.id));
    return { success: true };
  });

API Routes (Server Routes)

// routes/api/posts.ts
import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/api/posts')({
  server: {
    handlers: {
      GET: async () => {
        const posts = await db.query.posts.findMany();
        return Response.json(posts);
      },

      POST: async ({ request }) => {
        const body = await request.json();
        const result = schema.safeParse(body);

        if (!result.success) {
          return Response.json(
            { error: 'Validation failed', code: 'VALIDATION_ERROR' },
            { status: 400 },
          );
        }

        const [post] = await db.insert(posts).values(result.data).returning();
        return Response.json(post, { status: 201 });
      },
    },
  },
});

Middleware Chain

import { createServerFn, createMiddleware } from '@tanstack/react-start';

const authMiddleware = createMiddleware().server(async ({ next, request }) => {
  const session = await auth.api.getSession({ headers: request.headers });

  if (!session) {
    throw new Error('Unauthorized');
  }

  return next({ context: { user: session.user } });
});

const getProtectedData = createServerFn({ method: 'GET' })
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    return await fetchUserData(context.user.id);
  });

Environment Functions

import {
  createIsomorphicFn,
  createServerOnlyFn,
  createClientOnlyFn,
} from '@tanstack/react-start';

// Runs different code on client vs server
const getStorageValue = createIsomorphicFn()
  .server(() => process.env.FEATURE_FLAG)
  .client(() => localStorage.getItem('featureFlag'));

// Server-only (throws on client)
const readSecretConfig = createServerOnlyFn(() => {
  return process.env.SECRET_API_KEY;
});

// Client-only (throws on server)
const getGeolocation = createClientOnlyFn(() => {
  return navigator.geolocation.getCurrentPosition();
});

Error Handling Pattern

type ApiResult<T> = { data: T } | { error: string; code: string };

const updateUser = createServerFn({ method: 'POST' })
  .inputValidator(schema)
  .handler(async ({ data }): Promise<ApiResult<User>> => {
    try {
      const [user] = await db.update(users).set(data).returning();
      return { data: user };
    } catch (error) {
      console.error('updateUser failed:', error);
      return { error: 'Update failed', code: 'INTERNAL_ERROR' };
    }
  });

Standard Error Codes

CodeStatusDescription
AUTH_REQUIRED401User must be authenticated
FORBIDDEN403User lacks permission
NOT_FOUND404Resource doesn't exist
VALIDATION_ERROR400Invalid input data
CONFLICT409Resource already exists
INTERNAL_ERROR500Server error

Common Mistakes

MistakeCorrect Pattern
Missing auth checkAlways verify session from request headers
Throwing instead of returning errorReturn { error, code } format
Not validating inputAlways use .inputValidator(schema)
Not validating inputAlways use .inputValidator(schema)
Using GET for mutationsUse POST for create/update/delete
Not logging server errorsUse console.error() before returning
Exposing raw DB errorsCatch and return user-friendly messages
Missing error codeAlways include both error and code
Returning sensitive data in errorsOnly return what the client needs
Not handling validation errors in POSTCheck result.success and return 400

Delegation

  • Auth patterns: For authentication, see auth skill
  • Database queries: For Drizzle patterns, see database skill
  • Route integration: For loader patterns, see tanstack-router skill
  • Code review: After implementing server functions, delegate to code-reviewer agent

Topic References

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon