From b8b1c198019bedb7bc8d0b75c0d51a2c75e31352 Mon Sep 17 00:00:00 2001 From: Jordan Smith Date: Mon, 6 Oct 2025 14:56:45 -0400 Subject: [PATCH 1/3] Set up repo structure and backend connection --- .gitignore | 0 server/express/.env.example | 10 + server/express/.gitignore | 26 +++ server/express/package.json | 29 +++ server/express/src/app.ts | 117 ++++++++++ server/express/src/config/database.ts | 116 ++++++++++ .../src/controllers/movieController.ts | 29 +++ server/express/src/routes/movies.ts | 19 ++ server/express/src/types/index.ts | 183 ++++++++++++++++ server/express/src/utils/errorHandler.ts | 206 ++++++++++++++++++ server/express/tsconfig.json | 24 ++ 11 files changed, 759 insertions(+) create mode 100644 .gitignore create mode 100644 server/express/.env.example create mode 100644 server/express/.gitignore create mode 100644 server/express/package.json create mode 100644 server/express/src/app.ts create mode 100644 server/express/src/config/database.ts create mode 100644 server/express/src/controllers/movieController.ts create mode 100644 server/express/src/routes/movies.ts create mode 100644 server/express/src/types/index.ts create mode 100644 server/express/src/utils/errorHandler.ts create mode 100644 server/express/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/server/express/.env.example b/server/express/.env.example new file mode 100644 index 0000000..c3551b3 --- /dev/null +++ b/server/express/.env.example @@ -0,0 +1,10 @@ +# MongoDB Configuration +# Replace with your MongoDB Atlas connection string +MONGODB_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority + +# Server Configuration +PORT=3001 +NODE_ENV=development + +# CORS Configuration (frontend URL) +CORS_ORIGIN=http://localhost:3000 \ No newline at end of file diff --git a/server/express/.gitignore b/server/express/.gitignore new file mode 100644 index 0000000..26db928 --- /dev/null +++ b/server/express/.gitignore @@ -0,0 +1,26 @@ +# Dependencies +node_modules/ +npm-debug.log* +package-lock.json + +# Environment variables +.env + +# Build output +dist/ + +# TypeScript +*.tsbuildinfo + +# Logs +logs +*.log + +# Test coverage +coverage/ + +# Optional npm cache directory +.npm + +# macOS +.DS_Store \ No newline at end of file diff --git a/server/express/package.json b/server/express/package.json new file mode 100644 index 0000000..2a9ab8d --- /dev/null +++ b/server/express/package.json @@ -0,0 +1,29 @@ +{ + "name": "Sample Mflix Express.js Backend", + "version": "1.0.0", + "description": "Express.js backend for MongoDB sample mflix application demonstrating CRUD operations, aggregations, search, and geospatial queries", + "license": "Apache-2.0", + "author": "Jordan Smith", + "type": "commonjs", + "main": "dist/app.ts", + "scripts": { + "build": "tsc", + "start": "node dist/app.ts", + "dev": "ts-node src/app.ts", + "test-setup": "ts-node src/test-setup.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "express": "^5.1.0", + "mongodb": "^6.3.0", + "dotenv": "^16.3.1", + "cors": "^2.8.5" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.10.5", + "@types/cors": "^2.8.17", + "typescript": "^5.3.3", + "ts-node": "^10.9.2" + } +} diff --git a/server/express/src/app.ts b/server/express/src/app.ts new file mode 100644 index 0000000..8557a33 --- /dev/null +++ b/server/express/src/app.ts @@ -0,0 +1,117 @@ +/** + * Express.js Backend for MongoDB Sample MFlix Application + * + * This application demonstrates MongoDB operations using the Node.js driver + * with TypeScript. The code prioritizes readability and educational value + * over performance optimization. + */ + +import express from 'express'; +import cors from 'cors'; +import dotenv from 'dotenv'; +import { connectToDatabase, verifyRequirements } from './config/database'; +import { errorHandler } from './utils/errorHandler'; +import moviesRouter from './routes/movies'; + +// Load environment variables from .env file +// This must be called before any other imports that use environment variables +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 3001; + +/** + * CORS Configuration + * Allows the frontend to communicate with this Express backend + * In production, this should be configured to only allow specific origins + */ +app.use(cors({ + origin: process.env.CORS_ORIGIN || 'http://localhost:3000', + credentials: true +})); + +/** + * Middleware Configuration + * Express.json() parses incoming JSON requests and puts the parsed data in req.body + * The limit is set to handle potentially large movie documents + */ +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +/** + * API Routes + * All movie-related CRUD operations are handled by the movies router + */ +app.use('/api/movies', moviesRouter); + +/** + * Root Endpoint + * Provides basic information about the API + */ +app.get('/', (req, res) => { + res.json({ + name: 'MongoDB Sample MFlix API', + version: '1.0.0', + description: 'Express.js backend demonstrating MongoDB operations with the sample_mflix dataset', + endpoints: { + movies: '/api/movies' + } + }); +}); + +/** + * Global Error Handler + * This middleware catches any unhandled errors and returns a consistent error response + * It should be the last middleware in the chain + */ +app.use(errorHandler); + +/** + * Application Startup Function + * Handles database connection, requirement verification, and server startup + */ +async function startServer() { + try { + console.log('Starting MongoDB Sample MFlix API...'); + + // Connect to MongoDB database + console.log('Connecting to MongoDB...'); + await connectToDatabase(); + console.log('Connected to MongoDB successfully'); + + // Verify that all required indexes and sample data exist + console.log('Verifying requirements (indexes and sample data)...'); + await verifyRequirements(); + console.log('All requirements verified successfully'); + + // Start the Express server + app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`API documentation available at http://localhost:${PORT}`); + }); + + } catch (error) { + console.error('Failed to start server:', error); + + // Exit the process if we can't start properly + // This ensures the application doesn't run in a broken state + process.exit(1); + } +} + +/** + * Graceful Shutdown Handler + * Ensures the application shuts down cleanly when terminated + */ +process.on('SIGINT', () => { + console.log('\nReceived SIGINT. Shutting down...'); + process.exit(0); +}); + +process.on('SIGTERM', () => { + console.log('\nReceived SIGTERM. Shutting down...'); + process.exit(0); +}); + +// Start the server +startServer(); \ No newline at end of file diff --git a/server/express/src/config/database.ts b/server/express/src/config/database.ts new file mode 100644 index 0000000..840219f --- /dev/null +++ b/server/express/src/config/database.ts @@ -0,0 +1,116 @@ +/** + * Database Configuration and Connection Management + * + * This module handles MongoDB connection setup using the Node.js driver + * and implements pre-flight checks to ensure the application has all + * necessary indexes and sample data. + */ + +import { MongoClient, Db, Collection } from 'mongodb'; + +let client: MongoClient; +let database: Db; + +/** + * Establishes connection to MongoDB by using the connection string from environment variables + * + * @returns Promise - The connected database instance + * @throws Error if connection fails or if MONGODB_URI is not provided + */ +export async function connectToDatabase(): Promise { + // Return existing connection if already established + // This prevents creating multiple connections unnecessarily + if (database) { + return database; + } + + // Retrieve MongoDB connection string from environment variables + const uri = process.env.MONGODB_URI; + + if (!uri) { + throw new Error( + 'MONGODB_URI environment variable is not defined. Please check your .env file and ensure it contains a valid MongoDB connection string.' + ); + } + + try { + // Create new MongoDB client instance + client = new MongoClient(uri); + + // Connect to MongoDB + await client.connect(); + + // Get reference to the sample_mflix database + database = client.db('sample_mflix'); + + console.log(`Connected to database: ${database.databaseName}`); + + return database; + + } catch (error) { + throw new Error( + `Database connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } +} + +/** + * Gets a reference to a specific collection in the database + * + * @param collectionName - Name of the collection to access + * @returns Collection instance + * @throws Error if database is not connected + */ +export function getCollection(collectionName: string): Collection { + if (!database) { + throw new Error( + 'Database not connected.' + ); + } + + return database.collection(collectionName); +} + +/** + * Closes the database connection + * This should be called when the application is shutting down + */ +export async function closeDatabaseConnection(): Promise { + if (client) { + await client.close(); + console.log('Database connection closed'); + } +} + +/** + * Verifies that all required indexes exist and sample data is present + * + * If any requirements are missing, this function will attempt to create them. + */ +export async function verifyRequirements(): Promise { + try { + const db = await connectToDatabase(); + + // Check if the movies collection exists and has data + await verifyMoviesCollection(db); + console.log('All database requirements verified successfully'); + + } catch (error) { + console.error('Requirements verification failed:', error); + throw error; + } +} + +/** + * Verifies the movies collection and creates necessary indexes + */ +async function verifyMoviesCollection(db: Db): Promise { + const moviesCollection = db.collection('movies'); + + // Check if collection has documents + const movieCount = await moviesCollection.estimatedDocumentCount(); + + if (movieCount === 0) { + console.warn('Movies collection is empty. Please ensure sample_mflix data is loaded.'); + } +} diff --git a/server/express/src/controllers/movieController.ts b/server/express/src/controllers/movieController.ts new file mode 100644 index 0000000..d64e8c0 --- /dev/null +++ b/server/express/src/controllers/movieController.ts @@ -0,0 +1,29 @@ +/** + * Movie Controller + */ + +import { Request, Response } from 'express'; +import { getCollection } from '../config/database'; +import { createSuccessResponse } from '../utils/errorHandler'; + + +/** + * GET /api/movies + */ +export async function getAllMovies(req: Request, res: Response): Promise { + const moviesCollection = getCollection('movies'); + + try { + // Execute the find operation with all options + const movies = await moviesCollection + .find({}) + .limit(10) // TODO: Remove temp limit used for testing + .toArray(); + + // Return successful response + res.json(createSuccessResponse(movies, `Found ${movies.length} movies`)); + + } catch (error) { + throw new Error(`Failed to retrieve movies: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} \ No newline at end of file diff --git a/server/express/src/routes/movies.ts b/server/express/src/routes/movies.ts new file mode 100644 index 0000000..ecaac35 --- /dev/null +++ b/server/express/src/routes/movies.ts @@ -0,0 +1,19 @@ +/** + * Movies API Routes + */ + +import express from 'express'; +import { asyncHandler } from '../utils/errorHandler'; +import * as movieController from '../controllers/movieController'; + +const router = express.Router(); + +/** + * GET /api/movies + * + * Retrieves multiple movies with optional filtering, sorting, and pagination. + * Demonstrates the find() operation with various query options. + */ +router.get('/', asyncHandler(movieController.getAllMovies)); + +export default router; \ No newline at end of file diff --git a/server/express/src/types/index.ts b/server/express/src/types/index.ts new file mode 100644 index 0000000..18cafb9 --- /dev/null +++ b/server/express/src/types/index.ts @@ -0,0 +1,183 @@ +/** + * TypeScript Type Definitions for MongoDB Documents + * + * These interfaces define the structure of documents in the sample_mflix database. + * They help ensure type safety when working with MongoDB operations. + */ + +import { ObjectId } from 'mongodb'; + +/** + * Interface for Movie documents in the movies collection + * + * This represents the structure of movie documents in the sample_mflix.movies collection. + */ +export interface Movie { + _id?: ObjectId; + title: string; + year?: number; + plot?: string; + fullplot?: string; + released?: Date; + runtime?: number; + poster?: string; + genres?: string[]; + directors?: string[]; + writers?: string[]; + cast?: string[]; + countries?: string[]; + languages?: string[]; + rated?: string; + awards?: { + wins?: number; + nominations?: number; + text?: string; + }; + + imdb?: { + rating?: number; + votes?: number; + id?: number; + }; + + tomatoes?: { + viewer?: { + rating?: number; + numReviews?: number; + meter?: number; + }; + critic?: { + rating?: number; + numReviews?: number; + meter?: number; + }; + fresh?: number; + rotten?: number; + production?: string; + lastUpdated?: Date; + }; + + metacritic?: number; + type?: string; +} + +/** + * Interface for Theater documents in the theaters collection + */ +export interface Theater { + _id?: ObjectId; + theaterId: number; + location: { + address: { + street1: string; + city: string; + state: string; + zipcode: string; + }; + geo: { + type: 'Point'; + coordinates: [number, number]; // [longitude, latitude] + }; + }; +} + +/** + * Interface for Comment documents in the comments collection + */ +export interface Comment { + _id?: ObjectId; + name: string; + email: string; + movie_id: ObjectId; + text: string; + date: Date; +} + +/** + * Interface for API request bodies when creating/updating movies + */ +export interface CreateMovieRequest { + title: string; + year?: number; + plot?: string; + fullplot?: string; + genres?: string[]; + directors?: string[]; + writers?: string[]; + cast?: string[]; + countries?: string[]; + languages?: string[]; + rated?: string; + runtime?: number; + poster?: string; +} + +/** + * Interface for API request bodies when updating movies + * All fields are optional for partial updates + */ +export interface UpdateMovieRequest { + title?: string; + year?: number; + plot?: string; + fullplot?: string; + genres?: string[]; + directors?: string[]; + writers?: string[]; + cast?: string[]; + countries?: string[]; + languages?: string[]; + rated?: string; + runtime?: number; + poster?: string; +} + +/** + * Interface for search query parameters + */ +export interface SearchQuery { + q?: string; + genre?: string; + year?: number; + minRating?: number; + maxRating?: number; + limit?: number; + skip?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} + +/** + * Standard API response wrapper + */ +export interface ApiResponse { + success: boolean; + message?: string; + data?: T; + error?: { + message: string; + code?: string; + details?: any; + }; + timestamp: string; + pagination?: { + page: number; + limit: number; + total: number; + pages: number; + }; +} + +/** + * Type for MongoDB operation results + */ +export interface OperationResult { + acknowledged: boolean; + insertedId?: ObjectId; + insertedIds?: ObjectId[]; + modifiedCount?: number; + deletedCount?: number; + matchedCount?: number; + upsertedCount?: number; + upsertedId?: ObjectId; +} \ No newline at end of file diff --git a/server/express/src/utils/errorHandler.ts b/server/express/src/utils/errorHandler.ts new file mode 100644 index 0000000..397e9b5 --- /dev/null +++ b/server/express/src/utils/errorHandler.ts @@ -0,0 +1,206 @@ +/** + * Error Handling Utilities + * + * This module provides centralized error handling for the Express application. + * It includes middleware for catching and formatting errors in a consistent way. + */ + +import { Request, Response, NextFunction } from 'express'; +import { MongoError } from 'mongodb'; + +/** + * Interface for standardized error responses + */ +interface ErrorResponse { + success: false; + error: { + message: string; + code?: string; + details?: any; + }; + timestamp: string; +} + +/** + * Global error handling middleware + * + * This middleware catches all unhandled errors and returns a consistent + * error response format. It should be the last middleware in the chain. + * + * @param err - The error that was thrown + * @param req - Express request object + * @param res - Express response object + * @param next - Express next function + */ +export function errorHandler( + err: Error, + req: Request, + res: Response, + next: NextFunction +): void { + // Log the error for debugging purposes + // In production, we recommend using a logging service + console.error('Error occurred:', { + message: err.message, + stack: err.stack, + url: req.url, + method: req.method, + timestamp: new Date().toISOString() + }); + + // Determine the appropriate HTTP status code and error message + const errorResponse = createErrorResponse(err); + + // Send the error response + res.status(errorResponse.statusCode).json({ + success: false, + error: { + message: errorResponse.message, + code: errorResponse.code, + details: errorResponse.details + }, + timestamp: new Date().toISOString() + } as ErrorResponse); +} + +/** + * Creates a standardized error response based on the error type + * + * @param err - The error to process + * @returns Object containing status code, message, and optional details + */ +function createErrorResponse(err: Error): { + statusCode: number; + message: string; + code?: string; + details?: any; +} { + // Handle MongoDB-specific errors + if (err instanceof MongoError) { + return handleMongoError(err); + } + + // Handle validation errors (you can extend this for custom validation) + if (err.name === 'ValidationError') { + return { + statusCode: 400, + message: 'Invalid input data', + code: 'VALIDATION_ERROR', + details: err.message + }; + } + + // Handle JSON parsing errors + if (err instanceof SyntaxError && 'body' in err) { + return { + statusCode: 400, + message: 'Invalid JSON in request body', + code: 'JSON_PARSE_ERROR' + }; + } + + // Handle general application errors + return { + statusCode: 500, + message: 'An unexpected error occurred', + code: 'INTERNAL_SERVER_ERROR', + details: process.env.NODE_ENV === 'development' ? err.message : undefined + }; +} + +/** + * Handles MongoDB-specific errors and returns appropriate responses + * + * @param err - MongoDB error instance + * @returns Object with status code and message + */ +function handleMongoError(err: MongoError): { + statusCode: number; + message: string; + code: string; + details?: any; +} { + switch (err.code) { + case 11000: + // Duplicate key error + return { + statusCode: 409, + message: 'Duplicate entry found', + code: 'DUPLICATE_KEY_ERROR', + details: 'A document with this data already exists' + }; + + case 121: + // Document validation failed + return { + statusCode: 400, + message: 'Document validation failed', + code: 'DOCUMENT_VALIDATION_ERROR', + details: err.message + }; + + default: + // Generic MongoDB error + return { + statusCode: 500, + message: 'Database operation failed', + code: 'MONGODB_ERROR', + details: process.env.NODE_ENV === 'development' ? err.message : undefined + }; + } +} + +/** + * Async wrapper function for route handlers + * + * This function wraps async route handlers to automatically catch + * and forward any errors to the error handling middleware. + * + * Usage: + * app.get('/route', asyncHandler(async (req, res) => { + * // Your async code here + * })); + * + * @param fn - Async route handler function + * @returns Express middleware function + */ +export function asyncHandler( + fn: (req: Request, res: Response, next: NextFunction) => Promise +) { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +} + +/** + * Creates a standardized success response + * + * @param data - The data to include in the response + * @param message - Optional success message + * @returns Standardized success response object + */ +export function createSuccessResponse(data: any, message?: string) { + return { + success: true, + message: message || 'Operation completed successfully', + data, + timestamp: new Date().toISOString() + }; +} + +/** + * Validates that required fields are present in the request body + * + * @param body - Request body object + * @param requiredFields - Array of required field names + * @throws Error if any required fields are missing + */ +export function validateRequiredFields(body: any, requiredFields: string[]): void { + const missingFields = requiredFields.filter(field => + body[field] === undefined || body[field] === null || body[field] === '' + ); + + if (missingFields.length > 0) { + throw new Error(`Missing required fields: ${missingFields.join(', ')}`); + } +} \ No newline at end of file diff --git a/server/express/tsconfig.json b/server/express/tsconfig.json new file mode 100644 index 0000000..7fd5221 --- /dev/null +++ b/server/express/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file From 1d31ee7e0a950f2b07a1e3ded98e2472074ea690 Mon Sep 17 00:00:00 2001 From: Jordan Smith Date: Wed, 8 Oct 2025 12:51:23 -0400 Subject: [PATCH 2/3] feedback: --- server/express/package.json | 2 +- server/express/src/config/database.ts | 25 ++-- server/express/src/types/index.ts | 39 ++--- server/express/src/utils/errorHandler.ts | 177 +++++++++++------------ 4 files changed, 119 insertions(+), 124 deletions(-) diff --git a/server/express/package.json b/server/express/package.json index 2a9ab8d..bbe5f36 100644 --- a/server/express/package.json +++ b/server/express/package.json @@ -8,7 +8,7 @@ "main": "dist/app.ts", "scripts": { "build": "tsc", - "start": "node dist/app.ts", + "start": "node dist/app.js", "dev": "ts-node src/app.ts", "test-setup": "ts-node src/test-setup.ts", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/server/express/src/config/database.ts b/server/express/src/config/database.ts index 840219f..811c900 100644 --- a/server/express/src/config/database.ts +++ b/server/express/src/config/database.ts @@ -6,18 +6,12 @@ * necessary indexes and sample data. */ -import { MongoClient, Db, Collection } from 'mongodb'; +import { MongoClient, Db, Collection, Document } from 'mongodb'; let client: MongoClient; let database: Db; -/** - * Establishes connection to MongoDB by using the connection string from environment variables - * - * @returns Promise - The connected database instance - * @throws Error if connection fails or if MONGODB_URI is not provided - */ -export async function connectToDatabase(): Promise { +async function _connectToDatabase(): Promise { // Return existing connection if already established // This prevents creating multiple connections unnecessarily if (database) { @@ -54,6 +48,19 @@ export async function connectToDatabase(): Promise { } } +let connect$: Promise; +/** + * Establishes connection to MongoDB by using the connection string from environment variables + * + * @returns Promise - The connected database instance + * @throws Error if connection fails or if MONGODB_URI is not provided + */ +export async function connectToDatabase(): Promise { + // connect$ only gets assigned exactly once on the first request, ensuring all subsequent requests use the same connect$ promise. + connect$ ??= _connectToDatabase(); + return await connect$; +} + /** * Gets a reference to a specific collection in the database * @@ -61,7 +68,7 @@ export async function connectToDatabase(): Promise { * @returns Collection instance * @throws Error if database is not connected */ -export function getCollection(collectionName: string): Collection { +export function getCollection(collectionName: string): Collection { if (!database) { throw new Error( 'Database not connected.' diff --git a/server/express/src/types/index.ts b/server/express/src/types/index.ts index 18cafb9..cc18a04 100644 --- a/server/express/src/types/index.ts +++ b/server/express/src/types/index.ts @@ -147,18 +147,10 @@ export interface SearchQuery { sortOrder?: 'asc' | 'desc'; } -/** - * Standard API response wrapper - */ -export interface ApiResponse { - success: boolean; +export type SuccessResponse = { + success: true; message?: string; - data?: T; - error?: { - message: string; - code?: string; - details?: any; - }; + data: T; timestamp: string; pagination?: { page: number; @@ -168,16 +160,15 @@ export interface ApiResponse { }; } -/** - * Type for MongoDB operation results - */ -export interface OperationResult { - acknowledged: boolean; - insertedId?: ObjectId; - insertedIds?: ObjectId[]; - modifiedCount?: number; - deletedCount?: number; - matchedCount?: number; - upsertedCount?: number; - upsertedId?: ObjectId; -} \ No newline at end of file +export type ErrorResponse = { + success: false; + message: string; + error: { + message: string; + code?: string; + details?: any; + }; + timestamp: string; +} + +export type ApiResponse = SuccessResponse | ErrorResponse; \ No newline at end of file diff --git a/server/express/src/utils/errorHandler.ts b/server/express/src/utils/errorHandler.ts index 397e9b5..5ffbac6 100644 --- a/server/express/src/utils/errorHandler.ts +++ b/server/express/src/utils/errorHandler.ts @@ -7,18 +7,16 @@ import { Request, Response, NextFunction } from 'express'; import { MongoError } from 'mongodb'; +import { SuccessResponse, ErrorResponse } from '../types'; /** - * Interface for standardized error responses + * Custom ValidationError class for field validation errors */ -interface ErrorResponse { - success: false; - error: { - message: string; - code?: string; - details?: any; - }; - timestamp: string; +export class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + } } /** @@ -49,18 +47,16 @@ export function errorHandler( }); // Determine the appropriate HTTP status code and error message - const errorResponse = createErrorResponse(err); + const errorDetails = parseErrorDetails(err); + + const response: ErrorResponse = createErrorResponse( + errorDetails.message, + errorDetails.code, + errorDetails.details + ); // Send the error response - res.status(errorResponse.statusCode).json({ - success: false, - error: { - message: errorResponse.message, - code: errorResponse.code, - details: errorResponse.details - }, - timestamp: new Date().toISOString() - } as ErrorResponse); + res.status(errorDetails.statusCode).json(response); } /** @@ -69,86 +65,62 @@ export function errorHandler( * @param err - The error to process * @returns Object containing status code, message, and optional details */ -function createErrorResponse(err: Error): { - statusCode: number; +/** + * Internal helper function to parse error details and determine HTTP status codes + */ +function parseErrorDetails(err: Error): { message: string; - code?: string; + code: string; details?: any; + statusCode: number; } { - // Handle MongoDB-specific errors + // MongoDB specific error handling if (err instanceof MongoError) { - return handleMongoError(err); + switch (err.code) { + case 11000: + return { + message: 'Duplicate key error', + code: 'DUPLICATE_KEY', + details: 'A document with this data already exists', + statusCode: 409 + }; + case 121: + // Document validation failed + return { + statusCode: 400, + message: 'Document validation failed', + code: 'DOCUMENT_VALIDATION_ERROR', + details: err.message + }; + default: + return { + message: 'Database error', + code: 'DATABASE_ERROR', + details: err.code, + statusCode: 500 + }; + } } - - // Handle validation errors (you can extend this for custom validation) + + // Validation errors if (err.name === 'ValidationError') { return { - statusCode: 400, - message: 'Invalid input data', + message: 'Validation failed', code: 'VALIDATION_ERROR', - details: err.message + details: err.message, + statusCode: 400 }; } - - // Handle JSON parsing errors - if (err instanceof SyntaxError && 'body' in err) { - return { - statusCode: 400, - message: 'Invalid JSON in request body', - code: 'JSON_PARSE_ERROR' - }; - } - - // Handle general application errors + + // Default error handling return { - statusCode: 500, - message: 'An unexpected error occurred', - code: 'INTERNAL_SERVER_ERROR', - details: process.env.NODE_ENV === 'development' ? err.message : undefined + message: err.message || 'Internal server error', + code: 'INTERNAL_ERROR', + statusCode: 500 }; } -/** - * Handles MongoDB-specific errors and returns appropriate responses - * - * @param err - MongoDB error instance - * @returns Object with status code and message - */ -function handleMongoError(err: MongoError): { - statusCode: number; - message: string; - code: string; - details?: any; -} { - switch (err.code) { - case 11000: - // Duplicate key error - return { - statusCode: 409, - message: 'Duplicate entry found', - code: 'DUPLICATE_KEY_ERROR', - details: 'A document with this data already exists' - }; - - case 121: - // Document validation failed - return { - statusCode: 400, - message: 'Document validation failed', - code: 'DOCUMENT_VALIDATION_ERROR', - details: err.message - }; - - default: - // Generic MongoDB error - return { - statusCode: 500, - message: 'Database operation failed', - code: 'MONGODB_ERROR', - details: process.env.NODE_ENV === 'development' ? err.message : undefined - }; - } -} + /** * Async wrapper function for route handlers @@ -168,7 +140,11 @@ export function asyncHandler( fn: (req: Request, res: Response, next: NextFunction) => Promise ) { return (req: Request, res: Response, next: NextFunction) => { - Promise.resolve(fn(req, res, next)).catch(next); + try { + fn(req, res, next).catch(next); + } catch (error) { + next(error); + } }; } @@ -179,7 +155,7 @@ export function asyncHandler( * @param message - Optional success message * @returns Standardized success response object */ -export function createSuccessResponse(data: any, message?: string) { +export function createSuccessResponse(data: T, message?: string): SuccessResponse { return { success: true, message: message || 'Operation completed successfully', @@ -188,19 +164,40 @@ export function createSuccessResponse(data: any, message?: string) { }; } +/** + * Creates a standardized error response + * + * @param message - Error message + * @param code - Optional error code + * @param details - Optional error details + * @returns Standardized error response object + */ +export function createErrorResponse(message: string, code?: string, details?: any): ErrorResponse { + return { + success: false, + message, + error: { + message, + code, + details + }, + timestamp: new Date().toISOString() + }; +} + /** * Validates that required fields are present in the request body * * @param body - Request body object * @param requiredFields - Array of required field names - * @throws Error if any required fields are missing + * @throws ValidationError if any required fields are missing */ export function validateRequiredFields(body: any, requiredFields: string[]): void { const missingFields = requiredFields.filter(field => - body[field] === undefined || body[field] === null || body[field] === '' + body[field] == null || body[field] === '' ); if (missingFields.length > 0) { - throw new Error(`Missing required fields: ${missingFields.join(', ')}`); + throw new ValidationError(`Missing required fields: ${missingFields.join(', ')}`); } } \ No newline at end of file From 1f33a52bbc486ffe2665ca47309e3d6b39baed87 Mon Sep 17 00:00:00 2001 From: Jordan Smith Date: Fri, 10 Oct 2025 10:13:05 -0400 Subject: [PATCH 3/3] Cory feedback --- server/express/package.json | 2 +- server/express/src/app.ts | 4 +++- server/express/src/config/database.ts | 4 +--- server/express/src/controllers/movieController.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/express/package.json b/server/express/package.json index bbe5f36..d3a2a4b 100644 --- a/server/express/package.json +++ b/server/express/package.json @@ -1,5 +1,5 @@ { - "name": "Sample Mflix Express.js Backend", + "name": "sample-mflix-express-backend", "version": "1.0.0", "description": "Express.js backend for MongoDB sample mflix application demonstrating CRUD operations, aggregations, search, and geospatial queries", "license": "Apache-2.0", diff --git a/server/express/src/app.ts b/server/express/src/app.ts index 8557a33..9fb7130 100644 --- a/server/express/src/app.ts +++ b/server/express/src/app.ts @@ -9,7 +9,7 @@ import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; -import { connectToDatabase, verifyRequirements } from './config/database'; +import { closeDatabaseConnection, connectToDatabase, verifyRequirements } from './config/database'; import { errorHandler } from './utils/errorHandler'; import moviesRouter from './routes/movies'; @@ -105,11 +105,13 @@ async function startServer() { */ process.on('SIGINT', () => { console.log('\nReceived SIGINT. Shutting down...'); + closeDatabaseConnection(); process.exit(0); }); process.on('SIGTERM', () => { console.log('\nReceived SIGTERM. Shutting down...'); + closeDatabaseConnection(); process.exit(0); }); diff --git a/server/express/src/config/database.ts b/server/express/src/config/database.ts index 811c900..aa43cbb 100644 --- a/server/express/src/config/database.ts +++ b/server/express/src/config/database.ts @@ -42,9 +42,7 @@ async function _connectToDatabase(): Promise { return database; } catch (error) { - throw new Error( - `Database connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` - ); + throw error; } } diff --git a/server/express/src/controllers/movieController.ts b/server/express/src/controllers/movieController.ts index d64e8c0..d016bcc 100644 --- a/server/express/src/controllers/movieController.ts +++ b/server/express/src/controllers/movieController.ts @@ -24,6 +24,6 @@ export async function getAllMovies(req: Request, res: Response): Promise { res.json(createSuccessResponse(movies, `Found ${movies.length} movies`)); } catch (error) { - throw new Error(`Failed to retrieve movies: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; } } \ No newline at end of file