From f4df3df6982d5233089d8dd8560b0f441a86e508 Mon Sep 17 00:00:00 2001 From: Decobus Date: Sat, 19 Jul 2025 05:47:03 -0400 Subject: [PATCH] Implement professional API error handling infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API Infrastructure Improvements: - Created standardized API helper utilities for consistent error handling - Added comprehensive request validation with detailed error messages - Implemented proper HTTP status codes for different error types - Added request body parsing with validation helpers Enhanced Teams API: - Added input validation with length and format checks - Implemented duplicate team name prevention - Added proper error categorization (validation, database, server errors) - Consistent response format with timestamps - Improved error messages for better debugging Professional Standards: - Standardized error response structure across all APIs - Added development vs production error detail filtering - Implemented async error boundary wrapper for unhandled exceptions - Enhanced logging with structured error information - Added proper sorting for team list responses Foundation for Future APIs: - Reusable error handling patterns for all API routes - Validation helpers that can be extended to other endpoints - Consistent response structure for frontend integration - Better debugging capabilities for development 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/api/teams/route.ts | 115 +++++++++++++++++++++++-------- lib/apiHelpers.ts | 153 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+), 28 deletions(-) create mode 100644 lib/apiHelpers.ts diff --git a/app/api/teams/route.ts b/app/api/teams/route.ts index 8fe190d..ce99d57 100644 --- a/app/api/teams/route.ts +++ b/app/api/teams/route.ts @@ -2,36 +2,95 @@ import { NextResponse } from 'next/server'; import { getDatabase } from '../../../lib/database'; import { Team } from '@/types'; import { TABLE_NAMES } from '@/lib/constants'; +import { + withErrorHandling, + createSuccessResponse, + createValidationError, + createDatabaseError, + parseRequestBody +} from '@/lib/apiHelpers'; -export async function GET() { - const db = await getDatabase(); - const teams: Team[] = await db.all(`SELECT * FROM ${TABLE_NAMES.TEAMS}`); - return NextResponse.json(teams); +// Validation for team creation +function validateTeamInput(data: unknown): { + valid: boolean; + data?: { team_name: string }; + errors?: Record +} { + const errors: Record = {}; + + if (!data || typeof data !== 'object') { + errors.general = 'Request body must be an object'; + return { valid: false, errors }; + } + + const { team_name } = data as { team_name?: unknown }; + + if (!team_name || typeof team_name !== 'string') { + errors.team_name = 'Team name is required and must be a string'; + } else if (team_name.trim().length < 2) { + errors.team_name = 'Team name must be at least 2 characters long'; + } else if (team_name.trim().length > 50) { + errors.team_name = 'Team name must be less than 50 characters long'; + } + + if (Object.keys(errors).length > 0) { + return { valid: false, errors }; + } + + return { + valid: true, + data: { team_name: team_name.trim() } + }; } -export async function POST(request: Request) { - try { - const { team_name } = await request.json(); - - if (!team_name) { - return NextResponse.json({ error: 'Team name is required' }, { status: 400 }); - } +export const GET = withErrorHandling(async () => { + try { + const db = await getDatabase(); + const teams: Team[] = await db.all(`SELECT * FROM ${TABLE_NAMES.TEAMS} ORDER BY team_name ASC`); + + return createSuccessResponse(teams); + } catch (error) { + return createDatabaseError('fetch teams', error); + } +}); - const db = await getDatabase(); - - const result = await db.run( - `INSERT INTO ${TABLE_NAMES.TEAMS} (team_name) VALUES (?)`, - [team_name] - ); - - const newTeam: Team = { - team_id: result.lastID!, - team_name: team_name - }; - - return NextResponse.json(newTeam, { status: 201 }); - } catch (error) { - console.error('Error creating team:', error); - return NextResponse.json({ error: 'Failed to create team' }, { status: 500 }); +export const POST = withErrorHandling(async (request: Request) => { + const bodyResult = await parseRequestBody(request, validateTeamInput); + + if (!bodyResult.success) { + return bodyResult.response; + } + + const { team_name } = bodyResult.data; + + try { + const db = await getDatabase(); + + // Check if team name already exists + const existingTeam = await db.get( + `SELECT team_id FROM ${TABLE_NAMES.TEAMS} WHERE LOWER(team_name) = LOWER(?)`, + [team_name] + ); + + if (existingTeam) { + return createValidationError( + 'Team name already exists', + { team_name: 'A team with this name already exists' } + ); } -} \ No newline at end of file + + const result = await db.run( + `INSERT INTO ${TABLE_NAMES.TEAMS} (team_name) VALUES (?)`, + [team_name] + ); + + const newTeam: Team = { + team_id: result.lastID!, + team_name: team_name + }; + + return createSuccessResponse(newTeam, 201); + } catch (error) { + return createDatabaseError('create team', error); + } +}); \ No newline at end of file diff --git a/lib/apiHelpers.ts b/lib/apiHelpers.ts new file mode 100644 index 0000000..a85150d --- /dev/null +++ b/lib/apiHelpers.ts @@ -0,0 +1,153 @@ +import { NextResponse } from 'next/server'; + +// Standard error response structure +export interface APIError { + error: string; + message?: string; + details?: unknown; + timestamp: string; +} + +// Standard success response structure +export interface APISuccess { + success: true; + data: T; + timestamp: string; +} + +// Create standardized error response +export function createErrorResponse( + error: string, + status: number = 500, + message?: string, + details?: unknown +): NextResponse { + const errorResponse: APIError = { + error, + message, + details, + timestamp: new Date().toISOString(), + }; + + console.error(`API Error [${status}]:`, errorResponse); + + return NextResponse.json(errorResponse, { status }); +} + +// Create standardized success response +export function createSuccessResponse( + data: T, + status: number = 200 +): NextResponse { + const successResponse: APISuccess = { + success: true, + data, + timestamp: new Date().toISOString(), + }; + + return NextResponse.json(successResponse, { status }); +} + +// Validation error response +export function createValidationError( + message: string, + details?: Record +): NextResponse { + return createErrorResponse( + 'Validation Error', + 400, + message, + details + ); +} + +// Database error response +export function createDatabaseError( + operation: string, + originalError?: unknown +): NextResponse { + const message = `Database operation failed: ${operation}`; + return createErrorResponse( + 'Database Error', + 500, + message, + process.env.NODE_ENV === 'development' ? originalError : undefined + ); +} + +// OBS connection error response +export function createOBSError( + operation: string, + originalError?: unknown +): NextResponse { + const message = `OBS operation failed: ${operation}`; + return createErrorResponse( + 'OBS Error', + 502, + message, + process.env.NODE_ENV === 'development' ? originalError : undefined + ); +} + +// Wrap async API handlers with error handling +export function withErrorHandling( + handler: (...args: T) => Promise +) { + return async (...args: T): Promise => { + try { + return await handler(...args); + } catch (error) { + console.error('Unhandled API error:', error); + + if (error instanceof Error) { + return createErrorResponse( + 'Internal Server Error', + 500, + 'An unexpected error occurred', + process.env.NODE_ENV === 'development' ? error.stack : undefined + ); + } + + return createErrorResponse( + 'Internal Server Error', + 500, + 'An unknown error occurred' + ); + } + }; +} + +// Request body validation helper +export async function parseRequestBody( + request: Request, + validator?: (data: unknown) => { valid: boolean; data?: T; errors?: Record } +): Promise<{ success: true; data: T } | { success: false; response: NextResponse }> { + try { + const body = await request.json(); + + if (validator) { + const validation = validator(body); + if (!validation.valid) { + return { + success: false, + response: createValidationError( + 'Request validation failed', + validation.errors + ), + }; + } + return { success: true, data: validation.data! }; + } + + return { success: true, data: body as T }; + } catch (error) { + return { + success: false, + response: createErrorResponse( + 'Invalid Request', + 400, + 'Request body must be valid JSON' + ), + }; + } +} \ No newline at end of file