
Tanstack Start Api Routes
- 1 installs
- 3 repo stars
- Updated August 3, 2026
- netlify/swar-templates
Creates server-side API/HTTP endpoints in TanStack Start using the route server.handlers property for REST endpoints, webhooks, and file uploads.
About
Builds HTTP API routes (server routes) in TanStack Start via the server.handlers config, handling multiple methods, dynamic params, and non-HTML data responses. A developer uses it when adding REST endpoints or webhooks rather than RPC-style server functions.
- server.handlers with GET/POST and dynamic $param routes
- Distinguishes server routes from RPC server functions
Tanstack Start Api Routes by the numbers
- 1 all-time installs (skills.sh)
- Ranked #3,836 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/netlify/swar-templates --skill tanstack-start-api-routesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 3 |
| Last updated | August 3, 2026 |
| Repository | netlify/swar-templates ↗ |
What it does
Creates server-side API/HTTP endpoints in TanStack Start using the route server.handlers property for REST endpoints, webhooks, and file uploads.
Files
TanStack Start API Routes (Server Routes)
TanStack Start allows you to create API endpoints using the server property on routes. These run server-side and handle raw HTTP requests.
When to Use
- Building REST API endpoints
- Handling webhooks
- File uploads/downloads
- Any endpoint that returns data (not HTML)
Note: For RPC-style server logic callable from components, use server functions instead. Server routes are for traditional HTTP endpoints.
Basic Server Route
// src/routes/api/hello.ts
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/api/hello')({
server: {
handlers: {
GET: async ({ request }) => {
return new Response(JSON.stringify({ message: 'Hello, World!' }), {
headers: { 'Content-Type': 'application/json' },
});
},
},
},
});Multiple HTTP Methods
// src/routes/api/users.ts
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/api/users')({
server: {
handlers: {
GET: async ({ request }) => {
const users = await fetchUsers();
return Response.json(users);
},
POST: async ({ request }) => {
const body = await request.json();
const newUser = await createUser(body);
return Response.json(newUser, { status: 201 });
},
},
},
});Dynamic Parameters
// src/routes/api/users/$id.ts
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/api/users/$id')({
server: {
handlers: {
GET: async ({ params }) => {
const { id } = params;
const user = await getUser(id);
if (!user) {
return new Response('User not found', { status: 404 });
}
return Response.json(user);
},
PUT: async ({ request, params }) => {
const { id } = params;
const body = await request.json();
const updatedUser = await updateUser(id, body);
return Response.json(updatedUser);
},
DELETE: async ({ params }) => {
const { id } = params;
await deleteUser(id);
return new Response(null, { status: 204 });
},
},
},
});Multiple Dynamic Parameters
// src/routes/api/users/$userId/posts/$postId.ts
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/api/users/$userId/posts/$postId')({
server: {
handlers: {
GET: async ({ params }) => {
const { userId, postId } = params;
const post = await getUserPost(userId, postId);
return Response.json(post);
},
},
},
});Handler Context
The handler receives a context object with:
export const Route = createFileRoute('/api/example')({
server: {
handlers: {
GET: async (context) => {
// Request object (Web API Request)
const { request } = context;
// URL parameters from route
const { params } = context;
// Get headers
const authHeader = request.headers.get('Authorization');
// Get query parameters
const url = new URL(request.url);
const searchParams = url.searchParams;
const page = searchParams.get('page');
return Response.json({ page });
},
},
},
});Request Body Handling
JSON Body
POST: async ({ request }) => {
const body = await request.json();
// body is parsed JSON
return Response.json({ received: body });
},Form Data
POST: async ({ request }) => {
const formData = await request.formData();
const name = formData.get('name');
const email = formData.get('email');
return Response.json({ name, email });
},Raw Text/Binary
POST: async ({ request }) => {
const text = await request.text();
// or
const buffer = await request.arrayBuffer();
return new Response('Received', { status: 200 });
},Response Helpers
// JSON response
return Response.json({ data: 'value' });
// JSON with status
return Response.json({ error: 'Not found' }, { status: 404 });
// Plain text
return new Response('Hello', {
headers: { 'Content-Type': 'text/plain' },
});
// HTML
return new Response('<h1>Hello</h1>', {
headers: { 'Content-Type': 'text/html' },
});
// Redirect
return Response.redirect('https://example.com', 302);
// No content
return new Response(null, { status: 204 });
// Stream
const stream = new ReadableStream({ ... });
return new Response(stream, {
headers: { 'Content-Type': 'application/octet-stream' },
});Error Handling
export const Route = createFileRoute('/api/users/$id')({
server: {
handlers: {
GET: async ({ params }) => {
try {
const user = await getUser(params.id);
if (!user) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return Response.json(user);
} catch (error) {
console.error('Error fetching user:', error);
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
},
},
},
});Authentication
// src/routes/api/protected.ts
import { createFileRoute } from '@tanstack/react-router';
import { verifyToken } from '../lib/auth';
export const Route = createFileRoute('/api/protected')({
server: {
handlers: {
GET: async ({ request }) => {
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return Response.json(
{ error: 'Missing authorization header' },
{ status: 401 }
);
}
const token = authHeader.slice(7);
const user = await verifyToken(token);
if (!user) {
return Response.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
// Proceed with authenticated request
return Response.json({ user, message: 'Protected data' });
},
},
},
});CORS Headers
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
export const Route = createFileRoute('/api/data')({
server: {
handlers: {
// Handle preflight
OPTIONS: async () => {
return new Response(null, {
status: 204,
headers: corsHeaders,
});
},
GET: async ({ request }) => {
const data = await fetchData();
return Response.json(data, {
headers: corsHeaders,
});
},
},
},
});File Naming Conventions
| File Path | API Route |
|---|---|
routes/api/hello.ts | GET /api/hello |
routes/api/users.ts | GET /api/users |
routes/api/users/$id.ts | GET /api/users/:id |
routes/api/users.index.ts | GET /api/users |
routes/api/file/$.ts | GET /api/file/* (catch-all) |
Combined Route + API
A single file can handle both page rendering and API:
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/posts/$postId')({
// Server route handlers (API)
server: {
handlers: {
// GET /posts/123 with Accept: application/json → JSON response
GET: async ({ request, params }) => {
const accept = request.headers.get('Accept');
if (accept?.includes('application/json')) {
const post = await getPost(params.postId);
return Response.json(post);
}
// Fall through to page rendering
return undefined;
},
},
},
// Page loader and component
loader: async ({ params }) => {
const post = await getPost(params.postId);
return { post };
},
component: PostComponent,
});Webhooks Example
// src/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 sig = request.headers.get('stripe-signature');
const body = await request.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
sig!,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return Response.json(
{ error: 'Invalid signature' },
{ status: 400 }
);
}
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object);
break;
// Handle other events...
}
return Response.json({ received: true });
},
},
},
});Best Practices
1. Use server functions for RPC - If calling from components, prefer createServerFn 2. Validate input - Always validate request bodies and parameters 3. Handle errors - Return appropriate status codes and messages 4. Set correct headers - Content-Type, CORS, caching as needed 5. Keep handlers focused - One responsibility per endpoint 6. Use TypeScript - Type your request/response bodies