Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
netlify avatar

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-routes

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1
repo stars3
Last updatedAugust 3, 2026
Repositorynetlify/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

SKILL.mdMarkdownGitHub ↗

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 PathAPI Route
routes/api/hello.tsGET /api/hello
routes/api/users.tsGET /api/users
routes/api/users/$id.tsGET /api/users/:id
routes/api/users.index.tsGET /api/users
routes/api/file/$.tsGET /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

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.