forked from yusufipk/OpenFrame
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi-response.ts
More file actions
193 lines (169 loc) · 5.83 KB
/
Copy pathapi-response.ts
File metadata and controls
193 lines (169 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import { NextResponse } from 'next/server';
import { bigIntReplacer } from '@/lib/json-serialize';
/**
* Standardized API error response format
* All API routes should use this format for consistency
*/
export interface ApiErrorResponse {
error: string;
code?: string;
details?: Record<string, string[]>;
}
/**
* Standardized API success response format
*/
export interface ApiSuccessResponse<T = unknown> {
data: T;
meta?: {
page?: number;
limit?: number;
total?: number;
totalPages?: number;
};
}
/**
* HTTP status codes used in the API
*/
export const HttpStatus = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
UNPROCESSABLE_ENTITY: 422,
TOO_MANY_REQUESTS: 429,
INSUFFICIENT_STORAGE: 507,
INTERNAL_SERVER_ERROR: 500,
} as const;
/**
* Error codes for client-side handling
*/
export const ErrorCode = {
// Authentication errors
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
INVALID_CREDENTIALS: 'INVALID_CREDENTIALS',
// Resource errors
NOT_FOUND: 'NOT_FOUND',
ALREADY_EXISTS: 'ALREADY_EXISTS',
// Validation errors
VALIDATION_ERROR: 'VALIDATION_ERROR',
INVALID_INPUT: 'INVALID_INPUT',
// Rate limiting
RATE_LIMITED: 'RATE_LIMITED',
// Server errors
INTERNAL_ERROR: 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
// Storage errors
STORAGE_LIMIT_EXCEEDED: 'STORAGE_LIMIT_EXCEEDED',
/**
* Out of room because the account has not paid, rather than because the plan
* is full. Its own code so the client can offer the upgrade, which is the
* actual remedy here and is no help at all on the paid ceiling.
*/
TRIAL_STORAGE_LIMIT_EXCEEDED: 'TRIAL_STORAGE_LIMIT_EXCEEDED',
} as const;
/**
* Creates a standardized error response
*
* @param message - Human-readable error message
* @param status - HTTP status code
* @param code - Machine-readable error code for client handling
* @param details - Additional error details for validation errors (field -> messages[])
*
* @example
* ```ts
* return errorResponse("Project not found", 404, ErrorCode.NOT_FOUND);
* return errorResponse("Invalid input", 400, ErrorCode.VALIDATION_ERROR, { email: ["Invalid email format"] });
* ```
*/
export function errorResponse(
message: string,
status: number,
code?: string,
details?: Record<string, string[]>
): NextResponse<ApiErrorResponse> {
const body: ApiErrorResponse = { error: message };
if (code) body.code = code;
if (details) {
// Sanitize: only allow string arrays to prevent accidental data leakage
const sanitized: Record<string, string[]> = {};
for (const [key, value] of Object.entries(details)) {
if (Array.isArray(value) && value.every((v) => typeof v === 'string')) {
sanitized[key] = value;
}
}
if (Object.keys(sanitized).length > 0) {
body.details = sanitized;
}
}
return NextResponse.json(body, { status });
}
/**
* Creates a standardized success response
*
* @param data - Response data
* @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: [] });
* return successResponse({ projects: [] }, 200, { page: 1, limit: 10, total: 100 });
* ```
*/
export function successResponse<T>(
data: T,
status: number = HttpStatus.OK,
meta?: ApiSuccessResponse['meta']
): NextResponse<ApiSuccessResponse<T>> {
const body: ApiSuccessResponse<T> = { data };
if (meta) body.meta = meta;
return new NextResponse(JSON.stringify(body, bigIntReplacer), {
status,
headers: { 'content-type': 'application/json' },
}) as NextResponse<ApiSuccessResponse<T>>;
}
export function withCacheControl(response: Response, value: string): Response {
response.headers.set('Cache-Control', value);
return response;
}
/**
* Common error response helpers
*/
export const apiErrors = {
unauthorized: (message = 'Unauthorized') =>
errorResponse(message, HttpStatus.UNAUTHORIZED, ErrorCode.UNAUTHORIZED),
forbidden: (message = 'Forbidden') =>
errorResponse(message, HttpStatus.FORBIDDEN, ErrorCode.FORBIDDEN),
notFound: (resource = 'Resource') =>
errorResponse(`${resource} not found`, HttpStatus.NOT_FOUND, ErrorCode.NOT_FOUND),
badRequest: (message = 'Bad request') =>
errorResponse(message, HttpStatus.BAD_REQUEST, ErrorCode.INVALID_INPUT),
validationError: (message: string, details?: Record<string, string[]>) =>
errorResponse(message, HttpStatus.UNPROCESSABLE_ENTITY, ErrorCode.VALIDATION_ERROR, details),
conflict: (message: string) =>
errorResponse(message, HttpStatus.CONFLICT, ErrorCode.ALREADY_EXISTS),
rateLimited: (message = 'Too many requests') =>
errorResponse(message, HttpStatus.TOO_MANY_REQUESTS, ErrorCode.RATE_LIMITED),
internalError: (message = 'Internal server error') =>
errorResponse(message, HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_ERROR),
storageExceeded: (
message = 'Storage limit exceeded. Please delete some files to free up space.'
) => errorResponse(message, HttpStatus.INSUFFICIENT_STORAGE, ErrorCode.STORAGE_LIMIT_EXCEEDED),
/**
* The same 507, for an account that is out of room because it is on the free
* trial. Telling this caller to delete files is advice that does not apply:
* they have three gigabytes because they have not subscribed, not because they
* have filled two hundred.
*/
trialStorageExceeded: (message: string) =>
errorResponse(message, HttpStatus.INSUFFICIENT_STORAGE, ErrorCode.TRIAL_STORAGE_LIMIT_EXCEEDED),
};