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
Errorinstances →{ 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.