Files
fitaiProto/apps/admin/src/lib/api/responses.ts
T

365 lines
8.0 KiB
TypeScript

import { NextResponse } from "next/server";
import {
ApiSuccessResponse,
ApiErrorResponse,
PaginatedResponse,
ResponseMeta,
PaginationMeta,
ApiErrorCode,
} from "./types";
/**
* API Response Helpers for FitAI Admin API
*
* Provides consistent response formatting across all API endpoints.
*/
/**
* Generate a unique request ID
*/
function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
/**
* Create response metadata
*/
function createMeta(options?: {
requestId?: string;
pagination?: PaginationMeta;
}): ResponseMeta {
return {
timestamp: new Date().toISOString(),
requestId: options?.requestId || generateRequestId(),
...(options?.pagination && { pagination: options.pagination }),
};
}
/**
* Create a standardized success response
*
* @param data - The response data
* @param options - Optional metadata configuration
* @returns NextResponse with standardized format
*
* @example
* return successResponse({ user: { id: '123', email: 'user@example.com' } });
*
* @example
* return successResponse({ message: 'User created' }, { status: 201 });
*/
export function successResponse<T>(
data: T,
options?: {
status?: number;
requestId?: string;
headers?: HeadersInit;
},
): NextResponse<ApiSuccessResponse<T>> {
const response: ApiSuccessResponse<T> = {
success: true,
data,
meta: createMeta({ requestId: options?.requestId }),
};
return NextResponse.json(response, {
status: options?.status || 200,
headers: options?.headers,
});
}
/**
* Create a standardized paginated response
*
* @param data - Array of items to return
* @param pagination - Pagination metadata
* @param options - Optional configuration
* @returns NextResponse with paginated format
*
* @example
* const users = await db.getUsers();
* return paginatedResponse(users, {
* page: 1,
* pageSize: 20,
* totalItems: 100,
* totalPages: 5,
* hasNextPage: true,
* hasPreviousPage: false,
* });
*/
export function paginatedResponse<T>(
data: T[],
pagination: PaginationMeta,
options?: {
requestId?: string;
headers?: HeadersInit;
},
): NextResponse<PaginatedResponse<T>> {
const response: PaginatedResponse<T> = {
success: true,
data,
meta: {
...createMeta({ requestId: options?.requestId }),
pagination,
},
};
return NextResponse.json(response, {
status: 200,
headers: options?.headers,
});
}
/**
* Create a standardized error response
*
* @param message - Error message
* @param options - Error configuration
* @returns NextResponse with error format
*
* @example
* return errorResponse('User not found', {
* status: 404,
* code: ApiErrorCode.NOT_FOUND,
* });
*
* @example
* return errorResponse('Validation failed', {
* status: 400,
* code: ApiErrorCode.VALIDATION_ERROR,
* details: { email: ['Invalid email format'] },
* });
*/
export function errorResponse(
message: string,
options?: {
status?: number;
code?: ApiErrorCode | string;
details?: Record<string, string[]>;
requestId?: string;
headers?: HeadersInit;
},
): NextResponse<ApiErrorResponse> {
const response: ApiErrorResponse = {
success: false,
error: {
message,
code: options?.code,
details: options?.details,
},
meta: createMeta({ requestId: options?.requestId }),
};
return NextResponse.json(response, {
status: options?.status || 500,
headers: options?.headers,
});
}
/**
* Create a 400 Bad Request response
*/
export function badRequestResponse(
message: string = "Bad request",
details?: Record<string, string[]>,
headers?: HeadersInit,
): NextResponse<ApiErrorResponse> {
return errorResponse(message, {
status: 400,
code: ApiErrorCode.INVALID_INPUT,
details,
headers,
});
}
/**
* Create a 401 Unauthorized response
*/
export function unauthorizedResponse(
message: string = "Unauthorized",
headers?: HeadersInit,
): NextResponse<ApiErrorResponse> {
return errorResponse(message, {
status: 401,
code: ApiErrorCode.UNAUTHORIZED,
headers,
});
}
/**
* Create a 403 Forbidden response
*/
export function forbiddenResponse(
message: string = "Forbidden",
headers?: HeadersInit,
): NextResponse<ApiErrorResponse> {
return errorResponse(message, {
status: 403,
code: ApiErrorCode.FORBIDDEN,
headers,
});
}
/**
* Create a 404 Not Found response
*/
export function notFoundResponse(
message: string = "Resource not found",
headers?: HeadersInit,
): NextResponse<ApiErrorResponse> {
return errorResponse(message, {
status: 404,
code: ApiErrorCode.NOT_FOUND,
headers,
});
}
/**
* Create a 409 Conflict response
*/
export function conflictResponse(
message: string = "Resource conflict",
headers?: HeadersInit,
): NextResponse<ApiErrorResponse> {
return errorResponse(message, {
status: 409,
code: ApiErrorCode.CONFLICT,
headers,
});
}
/**
* Create a 500 Internal Server Error response
*/
export function internalErrorResponse(
message: string = "Internal server error",
headers?: HeadersInit,
): NextResponse<ApiErrorResponse> {
return errorResponse(message, {
status: 500,
code: ApiErrorCode.INTERNAL_ERROR,
headers,
});
}
/**
* Calculate pagination metadata from array and page parameters
*
* @param totalItems - Total number of items
* @param page - Current page number (1-indexed)
* @param pageSize - Number of items per page
* @returns Pagination metadata
*
* @example
* const pagination = calculatePagination(100, 1, 20);
* // { page: 1, pageSize: 20, totalItems: 100, totalPages: 5, hasNextPage: true, hasPreviousPage: false }
*/
export function calculatePagination(
totalItems: number,
page: number,
pageSize: number,
): PaginationMeta {
const totalPages = Math.ceil(totalItems / pageSize);
return {
page,
pageSize,
totalItems,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
};
}
/**
* Paginate an array of items
*
* @param items - Array to paginate
* @param page - Current page (1-indexed)
* @param pageSize - Items per page
* @returns Paginated slice of items and metadata
*
* @example
* const users = await db.getAllUsers();
* const { items, pagination } = paginateArray(users, 1, 20);
* return paginatedResponse(items, pagination);
*/
export function paginateArray<T>(
items: T[],
page: number,
pageSize: number,
): {
items: T[];
pagination: PaginationMeta;
} {
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedItems = items.slice(startIndex, endIndex);
return {
items: paginatedItems,
pagination: calculatePagination(items.length, page, pageSize),
};
}
/**
* Add CORS headers to response
*
* @param headers - Existing headers or undefined
* @returns Headers with CORS added
*/
export function addCorsHeaders(headers?: HeadersInit): HeadersInit {
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
if (!headers) {
return corsHeaders;
}
if (headers instanceof Headers) {
const newHeaders = new Headers(headers);
Object.entries(corsHeaders).forEach(([key, value]) => {
newHeaders.set(key, value);
});
return newHeaders;
}
if (Array.isArray(headers)) {
return [...headers, ...Object.entries(corsHeaders)];
}
return { ...headers, ...corsHeaders };
}
/**
* Create a 204 No Content response
*/
export function noContentResponse(
headers?: HeadersInit,
): NextResponse<undefined> {
return new NextResponse(undefined, {
status: 204,
headers,
});
}
/**
* Create a standardized created response (201)
*/
export function createdResponse<T>(
data: T,
options?: {
requestId?: string;
headers?: HeadersInit;
},
): NextResponse<ApiSuccessResponse<T>> {
return successResponse(data, {
status: 201,
requestId: options?.requestId,
headers: options?.headers,
});
}