diff --git a/app/api/projects/[projectId]/videos/[videoId]/versions/route.ts b/app/api/projects/[projectId]/videos/[videoId]/versions/route.ts index 44d60ea..2ab73ea 100644 --- a/app/api/projects/[projectId]/videos/[videoId]/versions/route.ts +++ b/app/api/projects/[projectId]/videos/[videoId]/versions/route.ts @@ -2,7 +2,6 @@ import { NextRequest } from 'next/server'; import { db } from '@/lib/db'; import { auth, checkProjectAccess } from '@/lib/auth'; import { validateUrl, validateOptionalUrlOrAppPath } from '@/lib/validation'; -import { toJsonSafe } from '@/lib/json-serialize'; import { rateLimit } from '@/lib/rate-limit'; import { notifyProjectOwner } from '@/lib/notifications'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; @@ -264,7 +263,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }).catch((err) => logError('Notification failed:', err)); } - const response = successResponse(toJsonSafe(version), 201); + const response = successResponse(version, 201); return withCacheControl(response, 'private, no-store'); } catch (error) { logError('Error creating version:', error); diff --git a/app/api/projects/[projectId]/videos/route.ts b/app/api/projects/[projectId]/videos/route.ts index 66d9838..86ce0fa 100644 --- a/app/api/projects/[projectId]/videos/route.ts +++ b/app/api/projects/[projectId]/videos/route.ts @@ -2,7 +2,6 @@ import { NextRequest } from 'next/server'; import { db } from '@/lib/db'; import { auth, checkProjectAccess } from '@/lib/auth'; import { validateUrl, validateOptionalUrlOrAppPath } from '@/lib/validation'; -import { toJsonSafe } from '@/lib/json-serialize'; import { rateLimit } from '@/lib/rate-limit'; import { notifyProjectOwner } from '@/lib/notifications'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; @@ -269,7 +268,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) { }).catch((err) => logError('Notification failed:', err)); } - const response = successResponse(toJsonSafe(video), 201); + const response = successResponse(video, 201); return withCacheControl(response, 'private, no-store'); } catch (error) { logError('Error creating video:', error); diff --git a/lib/api-response.ts b/lib/api-response.ts index 5a59d50..6afa021 100644 --- a/lib/api-response.ts +++ b/lib/api-response.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; +import { bigIntReplacer } from '@/lib/json-serialize'; /** * Standardized API error response format @@ -113,6 +114,11 @@ export function errorResponse( * @param status - HTTP status code (default: 200) * @param meta - Pagination or other metadata (optional) * + * Serialized with bigIntReplacer rather than NextResponse.json(), because + * JSON.stringify throws on BigInt and Prisma returns BigInt for sizeBytes. + * Any payload carrying a VideoVersion or VideoAsset row would otherwise 500 + * after its write had already committed. BigInt values render as strings. + * * @example * ```ts * return successResponse({ projects: [] }); @@ -127,7 +133,10 @@ export function successResponse( const body: ApiSuccessResponse = { data }; if (meta) body.meta = meta; - return NextResponse.json(body, { status }); + return new NextResponse(JSON.stringify(body, bigIntReplacer), { + status, + headers: { 'content-type': 'application/json' }, + }) as NextResponse>; } export function withCacheControl(response: Response, value: string): Response { diff --git a/lib/json-serialize.ts b/lib/json-serialize.ts index 39cc85a..9343256 100644 --- a/lib/json-serialize.ts +++ b/lib/json-serialize.ts @@ -1,8 +1,18 @@ +/** + * JSON.stringify replacer that renders Prisma BigInt columns (sizeBytes) as + * strings. Without it, JSON.stringify throws on any payload carrying one. + */ +export function bigIntReplacer(_key: string, value: unknown): unknown { + return typeof value === 'bigint' ? value.toString() : value; +} + /** * Converts values for JSON responses (e.g. Prisma BigInt fields). + * + * API routes do not need this — successResponse() serializes BigInt already. + * Use it for payloads that bypass that helper, such as data handed from a + * server component to a client component. */ export function toJsonSafe(value: T): T { - return JSON.parse( - JSON.stringify(value, (_key, val) => (typeof val === 'bigint' ? val.toString() : val)) - ) as T; + return JSON.parse(JSON.stringify(value, bigIntReplacer)) as T; }