Add API Endpoint#

The backend uses TanStack Start Server Routes. All API routes live under src/routes/v1/.

Create an Endpoint#

Create a file under src/routes/v1/ — the file path becomes the URL:

typescript// src/routes/v1/todos/list.ts
import { createFileRoute } from "@tanstack/react-router"
import { handler } from "@/server/utils/handler"
import { requireAuth } from "@/server/utils/auth"
import { query } from "@/server/shared/db"

export const Route = createFileRoute("/v1/todos/list")({
  server: {
    handlers: {
      GET: handler(async ({ request }) => {
        const user = await requireAuth(request)
        const r = await query(
          `SELECT * FROM todos WHERE user_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC`,
          [user.user_id]
        )
        return { success: true, message: "success", data: r.rows }
      }),
    },
  },
})
typescript// src/routes/v1/todos/create.ts
import { createFileRoute } from "@tanstack/react-router"
import { handler } from "@/server/utils/handler"
import { requireAuth } from "@/server/utils/auth"
import { query } from "@/server/shared/db"

export const Route = createFileRoute("/v1/todos/create")({
  server: {
    handlers: {
      POST: handler(async ({ request }) => {
        const user = await requireAuth(request)
        const { title } = await request.json()
        const r = await query(
          `INSERT INTO todos (user_id, title) VALUES ($1, $2) RETURNING *`,
          [user.user_id, title]
        )
        return { success: true, message: "success", data: r.rows[0] }
      }),
    },
  },
})

⚠️ The path in createFileRoute("...") must match the file path; otherwise the build fails.

What handler Wraps#

handler(fn) automatically handles:

  • Catching thrown Error instances → { success: false, message: error.message }
  • Serializing return value via Response.json(...)
  • Logging errors

Just throw new Error("...") in business code; the frontend receives { success: false, message: "..." }.

Unified Response Format#

All endpoints return:

json{ "success": true, "message": "success", "data": {} }

Errors:

json{ "success": false, "message": "Error description" }

The frontend http.ts checks success and surfaces message as a toast.

Public Endpoints#

Skip requireAuth:

typescriptexport const Route = createFileRoute("/v1/public/pricing")({
  server: {
    handlers: {
      GET: handler(async () => {
        const r = await query(`SELECT * FROM prices WHERE active = true`)
        return { success: true, message: "success", data: r.rows }
      }),
    },
  },
})

Where Does Business Logic Go?#

  • Route files (src/routes/v1/...): param validation, service call, response shape
  • Business logic (src/server/modules/<domain>/<domain>.service.ts): all SQL, external APIs, complex rules

Keep route files thin so logic stays reusable and testable.