Skip to content
api-routes logo

API Route Generator

api-routes

Generate secure TanStack Start API routes with authentication, rate limiting, validation, and proper error handling. Use when creating API endpoints, REST resources, or backend logic.

SKILL.md

Full skill instructions

API Route Generator

Create production-ready API endpoints following this project's security-first patterns.

Quick Start Template

import { createFileRoute } from '@tanstack/​react-router'
import { eq } from 'drizzle-orm'
import { db } from '@/​db'
import { tableName } from '@/​db/​schema'
import {
  errorResponse,
  requireAuth,
  simpleErrorResponse,
  successResponse,
} from '@/​lib/​api'
import { checkRateLimit } from '@/​lib/​rate-limit'

export const Route = createFileRoute('/​api/​resource')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        try {
          // 1. Rate limiting
          const rateLimit = await checkRateLimit(request, 'api')
          if (!rateLimit.allowed) {
            return new Response(
              JSON.stringify({ error: 'Too many requests' }),
              {
                status: 429,
                headers: { 'Retry-After': String(rateLimit.retryAfter) },
              },
            )
          }

          // 2. Authentication
          const auth = await requireAuth(request)
          if (!auth.success) return auth.response

          // 3. Business logic
          const items = await db.select().from(tableName)

          // 4. Success response
          return successResponse({ items })
        } catch (error) {
          return errorResponse('Failed to fetch', error)
        }
      },
    },
  },
})

Authentication Patterns

User Authentication (any logged-in user)

const auth = await requireAuth(request)
if (!auth.success) return auth.response
const user = auth.user // { id, email, role }

Admin Authentication

const auth = await requireAdmin(request)
if (!auth.success) return auth.response
// Only admins reach here

Optional Authentication (public with user context)

import { validateSession } from '@/​lib/​auth'

const session = await validateSession(request)
const userId = session.success ? session.user.id : null
// Proceed with or without user

Response Helpers

import { successResponse, simpleErrorResponse, errorResponse } from '@/​lib/​api'

// Success with data (200)
return successResponse({ items, total })

// Success with custom status
return successResponse({ item }, 201)

// Validation/​client error (400)
return simpleErrorResponse('Email is required')
return simpleErrorResponse('Not found', 404)

// Server error (logs stack in dev)
return errorResponse('Database error', error, 500)

Input Validation

Required Fields

const body = await request.json()
const { email, name, password } = body

if (!email?.trim()) {
  return simpleErrorResponse('Email is required')
}

if (!password || password.length < 8) {
  return simpleErrorResponse('Password must be at least 8 characters')
}

Localized String Validation

type LocalizedString = { en: string; fr?: string; id?: string }

if (!name || typeof name !== 'object' || !('en' in name) || !name.en?.trim()) {
  return simpleErrorResponse('Name must have a non-empty "en" property')
}

URL Parameter Validation

// For /​api/​resource/​$resourceId routes
const { resourceId } = params

if (!resourceId || !isValidUUID(resourceId)) {
  return simpleErrorResponse('Invalid resource ID', 400)
}

CRUD Operations

List with Pagination, Filtering, Sorting

import { and, asc, count, desc, eq, ilike, SQL } from 'drizzle-orm'

GET: async ({ request }) => {
  const auth = await requireAuth(request)
  if (!auth.success) return auth.response

  const url = new URL(request.url)

  // Pagination
  const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10))
  const limit = Math.min(
    100,
    Math.max(1, parseInt(url.searchParams.get('limit') || '10', 10)),
  )

  // Filtering
  const search = url.searchParams.get('q') || ''
  const status = url.searchParams.get('status') as 'active' | 'draft' | null

  // Sorting
  const sortKey = url.searchParams.get('sort') || 'createdAt'
  const sortOrder = url.searchParams.get('order') === 'asc' ? 'asc' : 'desc'

  // Build conditions
  const conditions: SQL[] = []
  if (search) {
    conditions.push(ilike(tableName.name, `%${search}%`) as SQL)
  }
  if (status) {
    conditions.push(eq(tableName.status, status))
  }
  const whereClause = conditions.length > 0 ? and(...conditions) : undefined

  // Get total count
  const [{ total }] = await db
    .select({ total: count() })
    .from(tableName)
    .where(whereClause)

  // Get paginated items
  const sortColumn =
    {
      name: tableName.name,
      status: tableName.status,
      createdAt: tableName.createdAt,
    }[sortKey] || tableName.createdAt

  const offset = (page - 1) * limit
  const items = await db
    .select()
    .from(tableName)
    .where(whereClause)
    .orderBy(sortOrder === 'asc' ? asc(sortColumn) : desc(sortColumn))
    .limit(limit)
    .offset(offset)

  return successResponse({
    items,
    total,
    page,
    limit,
    totalPages: Math.ceil(total / limit),
  })
}

Create with Transaction

POST: async ({ request }) => {
  const auth = await requireAdmin(request)
  if (!auth.success) return auth.response

  const body = await request.json()

  // Validate
  if (!body.name?.en?.trim()) {
    return simpleErrorResponse('Name is required')
  }

  try {
    const result = await db.transaction(async (tx) => {
      // Create main record
      const [item] = await tx
        .insert(tableName)
        .values({
          name: body.name,
          status: body.status || 'draft',
        })
        .returning()

      // Create related records
      if (body.variants?.length) {
        await tx.insert(variants).values(
          body.variants.map((v, i) => ({
            itemId: item.id,
            title: v.title,
            position: i,
          })),
        )
      }

      return item
    })

    return successResponse({ item: result }, 201)
  } catch (error) {
    return errorResponse('Failed to create', error)
  }
}

Update (PATCH)

PATCH: async ({ request, params }) => {
  const auth = await requireAdmin(request)
  if (!auth.success) return auth.response

  const { resourceId } = params
  const body = await request.json()

  // Check exists
  const [existing] = await db
    .select()
    .from(tableName)
    .where(eq(tableName.id, resourceId))
    .limit(1)

  if (!existing) {
    return simpleErrorResponse('Not found', 404)
  }

  // Update
  const [updated] = await db
    .update(tableName)
    .set({
      ...body,
      updatedAt: new Date(),
    })
    .where(eq(tableName.id, resourceId))
    .returning()

  return successResponse({ item: updated })
}

Delete

DELETE: async ({ request, params }) => {
  const auth = await requireAdmin(request)
  if (!auth.success) return auth.response

  const { resourceId } = params

  await db.delete(tableName).where(eq(tableName.id, resourceId))

  return successResponse({ deleted: true })
}

Avoiding N+1 Queries

// BAD: N+1 queries
const items = await db.select().from(orders)
for (const item of items) {
  const orderItems = await db
    .select()
    .from(orderItems)
    .where(eq(orderItems.orderId, item.id))
  item.items = orderItems
}

// GOOD: Batch with Map
const items = await db.select().from(orders)
const itemIds = items.map((i) => i.id)

// Single query for all related data
const allOrderItems = await db.select().from(orderItems)

// Build lookup map
const itemsByOrderId = new Map<string, typeof allOrderItems>()
for (const oi of allOrderItems) {
  const existing = itemsByOrderId.get(oi.orderId) || []
  existing.push(oi)
  itemsByOrderId.set(oi.orderId, existing)
}

// Use map
const itemsWithData = items.map((item) => ({
  ...item,
  orderItems: itemsByOrderId.get(item.id) || [],
}))

Rate Limiting

import { checkRateLimit } from '@/​lib/​rate-limit'

// Available tiers:
// 'auth': 5 requests per 15 minutes (login attempts)
// 'api': 100 requests per minute (general API)
// 'webhook': 50 requests per minute (payment webhooks)

const rateLimit = await checkRateLimit(request, 'api')
if (!rateLimit.allowed) {
  return new Response(JSON.stringify({ error: 'Too many requests' }), {
    status: 429,
    headers: { 'Retry-After': String(rateLimit.retryAfter) },
  })
}

Webhook Handlers

// src/​routes/​api/​webhooks/​stripe.ts
POST: async ({ request }) => {
  // Rate limit webhooks
  const rateLimit = await checkRateLimit(request, 'webhook')
  if (!rateLimit.allowed) {
    return new Response('Rate limited', { status: 429 })
  }

  // Verify signature
  const sig = request.headers.get('stripe-signature')
  if (!sig) {
    return new Response('Missing signature', { status: 400 })
  }

  const body = await request.text()

  try {
    const event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!,
    )

    switch (event.type) {
      case 'payment_intent.succeeded':
        await handlePaymentSuccess(event.data.object)
        break
      case 'payment_intent.payment_failed':
        await handlePaymentFailed(event.data.object)
        break
    }

    return new Response('OK', { status: 200 })
  } catch (error) {
    console.error('Webhook error:', error)
    return new Response('Webhook error', { status: 400 })
  }
}

Security Checklist

  • Rate limiting applied
  • Authentication checked
  • Input validated
  • SQL injection prevented (using Drizzle parameterized queries)
  • Sensitive data not exposed in responses
  • Error messages don't leak internals
  • CSRF protection for state-changing operations

File Naming

RouteFile
GET /​api/​productssrc/​routes/​api/​products/​index.ts
GET /​api/​products/:idsrc/​routes/​api/​products/​$productId.ts
POST /​api/​products/:id/​imagessrc/​routes/​api/​products/​$productId/​images.ts
POST /​api/​webhooks/​stripesrc/​routes/​api/​webhooks/​stripe.ts

See Also

  • src/​routes/​api/​products/​index.ts - Full CRUD example
  • src/​routes/​api/​orders/​$orderId.ts - Single resource
  • src/​routes/​api/​checkout/ - Complex flow
  • src/​lib/​api.ts - Response helpers
  • src/​lib/​rate-limit.ts - Rate limiting

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View