Fix API route patterns for TanStack Start

- Use createFileRoute with server.handlers pattern
- Export Route instead of APIRoute
This commit is contained in:
Nikita
2025-12-31 19:34:50 +02:00
parent ca05d547ed
commit 4d072aacb5
3 changed files with 169 additions and 104 deletions

View File

@@ -44,6 +44,7 @@ import { Route as ApiChatThreadsRouteImport } from './routes/api/chat-threads'
import { Route as ApiChatMessagesRouteImport } from './routes/api/chat-messages'
import { Route as ApiCanvasRouteImport } from './routes/api/canvas'
import { Route as ApiBrowserSessionsRouteImport } from './routes/api/browser-sessions'
import { Route as ApiBookmarksRouteImport } from './routes/api/bookmarks'
import { Route as ApiArchivesRouteImport } from './routes/api/archives'
import { Route as ApiApiKeysRouteImport } from './routes/api/api-keys'
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs'
@@ -59,6 +60,7 @@ import { Route as ApiStreamsUsernameRouteImport } from './routes/api/streams.$us
import { Route as ApiStreamSettingsRouteImport } from './routes/api/stream.settings'
import { Route as ApiStreamReplaysReplayIdRouteImport } from './routes/api/stream-replays.$replayId'
import { Route as ApiSpotifyNowPlayingRouteImport } from './routes/api/spotify.now-playing'
import { Route as ApiJazzCloudflareConfigRouteImport } from './routes/api/jazz.cloudflare-config'
import { Route as ApiFlowgladSplatRouteImport } from './routes/api/flowglad/$'
import { Route as ApiCreatorTiersRouteImport } from './routes/api/creator/tiers'
import { Route as ApiCreatorSubscribeRouteImport } from './routes/api/creator/subscribe'
@@ -256,6 +258,11 @@ const ApiBrowserSessionsRoute = ApiBrowserSessionsRouteImport.update({
path: '/api/browser-sessions',
getParentRoute: () => rootRouteImport,
} as any)
const ApiBookmarksRoute = ApiBookmarksRouteImport.update({
id: '/api/bookmarks',
path: '/api/bookmarks',
getParentRoute: () => rootRouteImport,
} as any)
const ApiArchivesRoute = ApiArchivesRouteImport.update({
id: '/api/archives',
path: '/api/archives',
@@ -332,6 +339,11 @@ const ApiSpotifyNowPlayingRoute = ApiSpotifyNowPlayingRouteImport.update({
path: '/api/spotify/now-playing',
getParentRoute: () => rootRouteImport,
} as any)
const ApiJazzCloudflareConfigRoute = ApiJazzCloudflareConfigRouteImport.update({
id: '/api/jazz/cloudflare-config',
path: '/api/jazz/cloudflare-config',
getParentRoute: () => rootRouteImport,
} as any)
const ApiFlowgladSplatRoute = ApiFlowgladSplatRouteImport.update({
id: '/api/flowglad/$',
path: '/api/flowglad/$',
@@ -462,6 +474,7 @@ export interface FileRoutesByFullPath {
'/users': typeof UsersRoute
'/api/api-keys': typeof ApiApiKeysRoute
'/api/archives': typeof ApiArchivesRouteWithChildren
'/api/bookmarks': typeof ApiBookmarksRoute
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
'/api/canvas': typeof ApiCanvasRouteWithChildren
'/api/chat-messages': typeof ApiChatMessagesRoute
@@ -493,6 +506,7 @@ export interface FileRoutesByFullPath {
'/api/creator/subscribe': typeof ApiCreatorSubscribeRoute
'/api/creator/tiers': typeof ApiCreatorTiersRoute
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
'/api/jazz/cloudflare-config': typeof ApiJazzCloudflareConfigRoute
'/api/spotify/now-playing': typeof ApiSpotifyNowPlayingRoute
'/api/stream-replays/$replayId': typeof ApiStreamReplaysReplayIdRoute
'/api/stream/settings': typeof ApiStreamSettingsRoute
@@ -534,6 +548,7 @@ export interface FileRoutesByTo {
'/users': typeof UsersRoute
'/api/api-keys': typeof ApiApiKeysRoute
'/api/archives': typeof ApiArchivesRouteWithChildren
'/api/bookmarks': typeof ApiBookmarksRoute
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
'/api/canvas': typeof ApiCanvasRouteWithChildren
'/api/chat-messages': typeof ApiChatMessagesRoute
@@ -565,6 +580,7 @@ export interface FileRoutesByTo {
'/api/creator/subscribe': typeof ApiCreatorSubscribeRoute
'/api/creator/tiers': typeof ApiCreatorTiersRoute
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
'/api/jazz/cloudflare-config': typeof ApiJazzCloudflareConfigRoute
'/api/spotify/now-playing': typeof ApiSpotifyNowPlayingRoute
'/api/stream-replays/$replayId': typeof ApiStreamReplaysReplayIdRoute
'/api/stream/settings': typeof ApiStreamSettingsRoute
@@ -608,6 +624,7 @@ export interface FileRoutesById {
'/users': typeof UsersRoute
'/api/api-keys': typeof ApiApiKeysRoute
'/api/archives': typeof ApiArchivesRouteWithChildren
'/api/bookmarks': typeof ApiBookmarksRoute
'/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren
'/api/canvas': typeof ApiCanvasRouteWithChildren
'/api/chat-messages': typeof ApiChatMessagesRoute
@@ -639,6 +656,7 @@ export interface FileRoutesById {
'/api/creator/subscribe': typeof ApiCreatorSubscribeRoute
'/api/creator/tiers': typeof ApiCreatorTiersRoute
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
'/api/jazz/cloudflare-config': typeof ApiJazzCloudflareConfigRoute
'/api/spotify/now-playing': typeof ApiSpotifyNowPlayingRoute
'/api/stream-replays/$replayId': typeof ApiStreamReplaysReplayIdRoute
'/api/stream/settings': typeof ApiStreamSettingsRoute
@@ -683,6 +701,7 @@ export interface FileRouteTypes {
| '/users'
| '/api/api-keys'
| '/api/archives'
| '/api/bookmarks'
| '/api/browser-sessions'
| '/api/canvas'
| '/api/chat-messages'
@@ -714,6 +733,7 @@ export interface FileRouteTypes {
| '/api/creator/subscribe'
| '/api/creator/tiers'
| '/api/flowglad/$'
| '/api/jazz/cloudflare-config'
| '/api/spotify/now-playing'
| '/api/stream-replays/$replayId'
| '/api/stream/settings'
@@ -755,6 +775,7 @@ export interface FileRouteTypes {
| '/users'
| '/api/api-keys'
| '/api/archives'
| '/api/bookmarks'
| '/api/browser-sessions'
| '/api/canvas'
| '/api/chat-messages'
@@ -786,6 +807,7 @@ export interface FileRouteTypes {
| '/api/creator/subscribe'
| '/api/creator/tiers'
| '/api/flowglad/$'
| '/api/jazz/cloudflare-config'
| '/api/spotify/now-playing'
| '/api/stream-replays/$replayId'
| '/api/stream/settings'
@@ -828,6 +850,7 @@ export interface FileRouteTypes {
| '/users'
| '/api/api-keys'
| '/api/archives'
| '/api/bookmarks'
| '/api/browser-sessions'
| '/api/canvas'
| '/api/chat-messages'
@@ -859,6 +882,7 @@ export interface FileRouteTypes {
| '/api/creator/subscribe'
| '/api/creator/tiers'
| '/api/flowglad/$'
| '/api/jazz/cloudflare-config'
| '/api/spotify/now-playing'
| '/api/stream-replays/$replayId'
| '/api/stream/settings'
@@ -902,6 +926,7 @@ export interface RootRouteChildren {
UsersRoute: typeof UsersRoute
ApiApiKeysRoute: typeof ApiApiKeysRoute
ApiArchivesRoute: typeof ApiArchivesRouteWithChildren
ApiBookmarksRoute: typeof ApiBookmarksRoute
ApiBrowserSessionsRoute: typeof ApiBrowserSessionsRouteWithChildren
ApiCanvasRoute: typeof ApiCanvasRouteWithChildren
ApiChatMessagesRoute: typeof ApiChatMessagesRoute
@@ -926,6 +951,7 @@ export interface RootRouteChildren {
ApiCreatorSubscribeRoute: typeof ApiCreatorSubscribeRoute
ApiCreatorTiersRoute: typeof ApiCreatorTiersRoute
ApiFlowgladSplatRoute: typeof ApiFlowgladSplatRoute
ApiJazzCloudflareConfigRoute: typeof ApiJazzCloudflareConfigRoute
ApiSpotifyNowPlayingRoute: typeof ApiSpotifyNowPlayingRoute
ApiStreamsUsernameRoute: typeof ApiStreamsUsernameRouteWithChildren
ApiStripeBillingRoute: typeof ApiStripeBillingRoute
@@ -1189,6 +1215,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiBrowserSessionsRouteImport
parentRoute: typeof rootRouteImport
}
'/api/bookmarks': {
id: '/api/bookmarks'
path: '/api/bookmarks'
fullPath: '/api/bookmarks'
preLoaderRoute: typeof ApiBookmarksRouteImport
parentRoute: typeof rootRouteImport
}
'/api/archives': {
id: '/api/archives'
path: '/api/archives'
@@ -1294,6 +1327,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiSpotifyNowPlayingRouteImport
parentRoute: typeof rootRouteImport
}
'/api/jazz/cloudflare-config': {
id: '/api/jazz/cloudflare-config'
path: '/api/jazz/cloudflare-config'
fullPath: '/api/jazz/cloudflare-config'
preLoaderRoute: typeof ApiJazzCloudflareConfigRouteImport
parentRoute: typeof rootRouteImport
}
'/api/flowglad/$': {
id: '/api/flowglad/$'
path: '/api/flowglad/$'
@@ -1611,6 +1651,7 @@ const rootRouteChildren: RootRouteChildren = {
UsersRoute: UsersRoute,
ApiApiKeysRoute: ApiApiKeysRoute,
ApiArchivesRoute: ApiArchivesRouteWithChildren,
ApiBookmarksRoute: ApiBookmarksRoute,
ApiBrowserSessionsRoute: ApiBrowserSessionsRouteWithChildren,
ApiCanvasRoute: ApiCanvasRouteWithChildren,
ApiChatMessagesRoute: ApiChatMessagesRoute,
@@ -1635,6 +1676,7 @@ const rootRouteChildren: RootRouteChildren = {
ApiCreatorSubscribeRoute: ApiCreatorSubscribeRoute,
ApiCreatorTiersRoute: ApiCreatorTiersRoute,
ApiFlowgladSplatRoute: ApiFlowgladSplatRoute,
ApiJazzCloudflareConfigRoute: ApiJazzCloudflareConfigRoute,
ApiSpotifyNowPlayingRoute: ApiSpotifyNowPlayingRoute,
ApiStreamsUsernameRoute: ApiStreamsUsernameRouteWithChildren,
ApiStripeBillingRoute: ApiStripeBillingRoute,

View File

@@ -1,8 +1,14 @@
import { createAPIFileRoute } from "@tanstack/react-start/api"
import { createFileRoute } from "@tanstack/react-router"
import { eq } from "drizzle-orm"
import { getDb } from "@/db/connection"
import { api_keys, bookmarks, users } from "@/db/schema"
const json = (data: unknown, status = 200) =>
new Response(JSON.stringify(data), {
status,
headers: { "content-type": "application/json" },
})
// Hash function for API key verification
async function hashApiKey(key: string): Promise<string> {
const encoder = new TextEncoder()
@@ -44,81 +50,91 @@ async function getUserFromApiKey(apiKey: string) {
return user || null
}
export const APIRoute = createAPIFileRoute("/api/bookmarks")({
// POST - Add a bookmark
POST: async ({ request }) => {
try {
const body = await request.json()
const { url, title, description, tags, api_key } = body
export const Route = createFileRoute("/api/bookmarks")({
server: {
handlers: {
// POST - Add a bookmark
POST: async ({ request }) => {
try {
const body = (await request.json()) as {
url?: string
title?: string
description?: string
tags?: string
api_key?: string
}
const { url, title, description, tags, api_key } = body
if (!url) {
return Response.json({ error: "URL is required" }, { status: 400 })
}
if (!url) {
return json({ error: "URL is required" }, 400)
}
if (!api_key) {
return Response.json({ error: "API key is required" }, { status: 401 })
}
if (!api_key) {
return json({ error: "API key is required" }, 401)
}
const user = await getUserFromApiKey(api_key)
if (!user) {
return Response.json({ error: "Invalid API key" }, { status: 401 })
}
const user = await getUserFromApiKey(api_key)
if (!user) {
return json({ error: "Invalid API key" }, 401)
}
const db = getDb(process.env.DATABASE_URL!)
const db = getDb(process.env.DATABASE_URL!)
// Insert bookmark
const [bookmark] = await db
.insert(bookmarks)
.values({
user_id: user.id,
url,
title: title || null,
description: description || null,
tags: tags || null,
})
.returning()
// Insert bookmark
const [bookmark] = await db
.insert(bookmarks)
.values({
user_id: user.id,
url,
title: title || null,
description: description || null,
tags: tags || null,
})
.returning()
return Response.json({
success: true,
bookmark: {
id: bookmark.id,
url: bookmark.url,
title: bookmark.title,
created_at: bookmark.created_at,
},
})
} catch (error) {
console.error("Error adding bookmark:", error)
return Response.json({ error: "Failed to add bookmark" }, { status: 500 })
}
},
return json({
success: true,
bookmark: {
id: bookmark.id,
url: bookmark.url,
title: bookmark.title,
created_at: bookmark.created_at,
},
})
} catch (error) {
console.error("Error adding bookmark:", error)
return json({ error: "Failed to add bookmark" }, 500)
}
},
// GET - List bookmarks (requires API key in header)
GET: async ({ request }) => {
try {
const apiKey = request.headers.get("x-api-key")
// GET - List bookmarks (requires API key in header)
GET: async ({ request }) => {
try {
const apiKey = request.headers.get("x-api-key")
if (!apiKey) {
return Response.json({ error: "API key is required" }, { status: 401 })
}
if (!apiKey) {
return json({ error: "API key is required" }, 401)
}
const user = await getUserFromApiKey(apiKey)
if (!user) {
return Response.json({ error: "Invalid API key" }, { status: 401 })
}
const user = await getUserFromApiKey(apiKey)
if (!user) {
return json({ error: "Invalid API key" }, 401)
}
const db = getDb(process.env.DATABASE_URL!)
const db = getDb(process.env.DATABASE_URL!)
const userBookmarks = await db
.select()
.from(bookmarks)
.where(eq(bookmarks.user_id, user.id))
.orderBy(bookmarks.created_at)
const userBookmarks = await db
.select()
.from(bookmarks)
.where(eq(bookmarks.user_id, user.id))
.orderBy(bookmarks.created_at)
return Response.json({ bookmarks: userBookmarks })
} catch (error) {
console.error("Error fetching bookmarks:", error)
return Response.json({ error: "Failed to fetch bookmarks" }, { status: 500 })
}
return json({ bookmarks: userBookmarks })
} catch (error) {
console.error("Error fetching bookmarks:", error)
return json({ error: "Failed to fetch bookmarks" }, 500)
}
},
},
},
})

View File

@@ -1,42 +1,49 @@
import { json } from "@tanstack/react-start"
import type { APIContext } from "@tanstack/react-router"
import { createFileRoute } from "@tanstack/react-router"
/**
* Get or set Cloudflare stream configuration from Jazz
*
* GET: Returns current Cloudflare Live Input UID
* PUT: Updates Cloudflare Live Input UID
*/
export async function GET({ request, context }: APIContext) {
try {
// For now, return the hardcoded value
// TODO: Read from Jazz when worker is set up
return json({
liveInputUid: "bb7858eafc85de6c92963f3817477b5d",
customerCode: "xctsztqzu046isdc",
name: "linsa-nikiv",
updatedAt: Date.now(),
})
} catch (error) {
return json({ error: "Failed to fetch config" }, { status: 500 })
}
}
const json = (data: unknown, status = 200) =>
new Response(JSON.stringify(data), {
status,
headers: { "content-type": "application/json" },
})
export async function PUT({ request, context }: APIContext) {
try {
const body = await request.json()
const { liveInputUid, customerCode, name } = body
export const Route = createFileRoute("/api/jazz/cloudflare-config")({
server: {
handlers: {
// GET: Returns current Cloudflare Live Input UID
GET: async () => {
try {
return json({
liveInputUid: "bb7858eafc85de6c92963f3817477b5d",
customerCode: "xctsztqzu046isdc",
name: "linsa-nikiv",
updatedAt: Date.now(),
})
} catch (error) {
return json({ error: "Failed to fetch config" }, 500)
}
},
// TODO: Write to Jazz when worker is set up
// For now, just return success
return json({
success: true,
liveInputUid,
customerCode,
name,
updatedAt: Date.now(),
})
} catch (error) {
return json({ error: "Failed to update config" }, { status: 500 })
}
}
// PUT: Updates Cloudflare Live Input UID
PUT: async ({ request }) => {
try {
const body = (await request.json()) as {
liveInputUid?: string
customerCode?: string
name?: string
}
const { liveInputUid, customerCode, name } = body
return json({
success: true,
liveInputUid,
customerCode,
name,
updatedAt: Date.now(),
})
} catch (error) {
return json({ error: "Failed to update config" }, 500)
}
},
},
},
})