name: tanstack-start-server-functions-fix
description: Fix broken /_serverFn 500 errors in TanStack Start (Lovable) projects — convert createServerFn to API route handlers when the build plugin doesn't register them in the manifest.
Fix TanStack Start /_serverFn 500 — Convert to API Routes
Symptom
All calls to POST /_serverFn return:
`json
{"status":500,"unhandled":true,"message":"HTTPError"}
`
Meanwhile GET / works, routes work, but server functions don't.
Root Cause
TanStack Start (v1.167.x used by Lovable) uses a Vite plugin (@lovable.dev/vite-tanstack-config) that runs a Babel transform during bun run build to scan route files for createServerFn calls and write them to a manifest. The server handler (dist/server/server.js) does manifest[id] lookups — if the manifest is empty, getServerFnById(id) throws and the error is caught as an HTTPError, returning 500.
The plugin only scans files that use createFileRoute (route files under src/routes/). If a subagent or developer puts createServerFn in a non-route file like src/server/gmail.functions.ts, it gets compiled away and the manifest stays empty (const manifest = {}).
Fix — Convert to API Route Handlers
Do NOT try to fix the build plugin. Replace the createServerFn system with standard TanStack Start API route handlers. This is more robust, has no build-time magic, and works 100% of the time.
Step 1 — Create the route handler file
Create the file under src/routes/api/gmail/_serverFn.ts (must be under src/routes/ to be a route):
`typescript
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
// Schemas
const SendSchema = z.object({
to: z.string().email(),
subject: z.string().min(1),
body: z.string().min(1),
documentIds: z.array(z.string().uuid()),
clientName: z.string(),
});
// Auth helper
async function getAuthClient(request: Request) {
const authHeader = request.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) throw new Response("Unauthorized", { status: 401 });
const token = authHeader.replace("Bearer ", "");
// ... create supabase client with token auth
return { supabase, userId };
}
export const Route = createFileRoute("/api/gmail/_serverFn")({
server: {
handlers: {
POST: async ({ request }) => {
let body: any;
try { body = await request.json(); } catch { return Response.json({ error: { message: "Invalid JSON" } }, { status: 400 }); }
const { __fn, data } = body;
switch (__fn) {
case "getGmailConnection": { / ... / return Response.json(result); }
case "sendGmailWithAttachments": { / ... / return Response.json({ ok: true }); }
default: return Response.json({ error: { message: Unknown: ${__fn} } }, { status: 404 });
}
},
},
},
});
`
Step 2 — Update client code
Remove useServerFn imports and manual fetch("/_serverFn", ...) calls. Replace with plain fetch() to the new route:
`typescript
async function gmailFetch(fn: string, data?: unknown) {
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error("Não autenticado");
const res = await fetch("/api/gmail/_serverFn", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: Bearer ${session.access_token},
},
body: JSON.stringify({ __fn: fn, data }),
});
const json = await res.json();
if (!res.ok || json.error) throw new Error(json.error?.message ?? "Erro");
return json;
}
`
Step 3 — Delete the old file
`bash
rm src/server/gmail.functions.ts
`
Step 4 — Build and deploy
`bash
bun run build # NOT npm run build — Lovable/TanStack uses Bun
rsync dist/ to VPS
pm2 restart appname
`
Key Differences
| Aspect | createServerFn | API Route Handler |
| -------- | ------------------ | ------------------ |
| Registration | Build plugin magic | Automatic (it's a route) |
| Manifest | Must be populated by plugin | No manifest needed |
| Route file required | Must use createFileRoute | Must be in src/routes/ |
| Debugging | Hard — plugin is Lovable-specific | Easy — standard HTTP |
| Extensibility | Limited | Full Express-like control |
Gotcha
- - File naming: use
_serverFn.tsor_handler.tssuffix — TanStack Start treats files starting with_as API routes (not page routes), so they won't create a page route. - - Build command is
bun run buildnotnpm run build— Lovable/TanStack Start projects use Bun. - - The
.envmust have the necessary environment variables (e.g.,GOOGLE_OAUTH_CLIENT_ID,GOOGLE_OAUTH_CLIENT_SECRET) — server-side routes can readprocess.env.