API Routers
Use Case: Define typed, validated API routes for MCP servers using TanStack Start's file-based routing and server functions. This pattern gives you automatic type inference, request validation, and a documented contract for agents.
Prerequisites
- TanStack Start project with file-based routing already set up
- TypeScript 5+ for full type inference
- Zod (optional but recommended) for input validation
Setup & Configuration
The route tree is auto-generated. Routes live in src/routes/ and are mapped to URL paths automatically by the file system convention:
text
src/routes/
index.tsx -> /
mcp-patterns/
index.tsx -> /mcp-patterns
database-queries.tsx -> /mcp-patterns/database-queries
api/
og.tsx -> /api/og (server-side OG image)Core Implementation
Define a server function with typed input/output. The validator runs on both the client (optimistic checks) and server (authoritative check):
typescript
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
const BookmarkSchema = z.object({
title: z.string().min(1).max(200),
url: z.string().url(),
tags: z.array(z.string()).max(10).default([]),
});
export const createBookmark = createServerFn({ method: "POST" })
.validator((d: unknown) => BookmarkSchema.parse(d))
.handler(async ({ data }) => {
// Persist to database (see database-queries pattern)
return { id: crypto.randomUUID(), ...data };
});
export const listBookmarks = createServerFn({ method: "GET" })
.validator((d: unknown) => z.object({ tag: z.string().optional() }).parse(d))
.handler(async ({ data }) => {
const db = getDb();
const result = data.tag
? await db.execute({ sql: "SELECT * FROM bookmarks WHERE ? IN (tags)", args: [data.tag] })
: await db.execute("SELECT * FROM bookmarks ORDER BY created_at DESC");
return result.rows;
});Call it from the client, wrapping the payload under a data key:
typescript
import { createBookmark, listBookmarks } from "./routes/bookmarks";
// CORRECT: wrap in { data }
const result = await createBookmark({
data: { title: "MCP Spec", url: "https://modelcontextprotocol.io", tags: ["mcp"] },
});
// WRONG: await createBookmark({ title: "MCP Spec", url: "..." })
// -> data is undefined on the server, handler crashes
// Query with optional filter
const allBookmarks = await listBookmarks({ data: {} });
const tagged = await listBookmarks({ data: { tag: "mcp" } });For MCP tool definitions, expose the function as a tool with a schema the agent can discover:
typescript
const MCP_TOOLS = [
{
name: "create_bookmark",
description: "Save a bookmark with optional tags",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Page title" },
url: { type: "string", format: "uri" },
tags: { type: "array", items: { type: "string" } },
},
required: ["title", "url"],
},
handler: async (args: Record<string, unknown>) => {
"use server";
return createBookmark({ data: args as any });
},
},
];Deployment Notes
- Method convention: Always pass arguments to
createServerFnwrapped in{ data }- top-level fields send an empty body to the server. - Error handling: Server functions throw on unhandled errors. Use try/catch and return structured error objects so the agent can react gracefully.
- Rate limits: Add a simple in-memory rate limiter for production. Serverless instances are ephemeral, so consider Turso or Redis for distributed rate tracking.
- CORS: MCP clients may call from different origins. Set CORS headers at the server function level or via Nitro hooks.
- Timeouts: Serverless functions have a 10-30s timeout. For long-running queries, use a polling pattern or stream results.