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..d3a2a4b --- /dev/null +++ b/server/express/package.json @@ -0,0 +1,29 @@ +{ + "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", + "author": "Jordan Smith", + "type": "commonjs", + "main": "dist/app.ts", + "scripts": { + "build": "tsc", + "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" + }, + "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..9fb7130 --- /dev/null +++ b/server/express/src/app.ts @@ -0,0 +1,119 @@ +/** + * 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 { closeDatabaseConnection, 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...'); + closeDatabaseConnection(); + process.exit(0); +}); + +process.on('SIGTERM', () => { + console.log('\nReceived SIGTERM. Shutting down...'); + closeDatabaseConnection(); + 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..aa43cbb --- /dev/null +++ b/server/express/src/config/database.ts @@ -0,0 +1,121 @@ +/** + * 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, Document } from 'mongodb'; + +let client: MongoClient; +let database: Db; + +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 error; + } +} + +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 + * + * @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..d016bcc --- /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 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..cc18a04 --- /dev/null +++ b/server/express/src/types/index.ts @@ -0,0 +1,174 @@ +/** + * 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'; +} + +export type SuccessResponse = { + success: true; + message?: string; + data: T; + timestamp: string; + pagination?: { + page: number; + limit: number; + total: number; + pages: number; + }; +} + +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 new file mode 100644 index 0000000..5ffbac6 --- /dev/null +++ b/server/express/src/utils/errorHandler.ts @@ -0,0 +1,203 @@ +/** + * 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'; +import { SuccessResponse, ErrorResponse } from '../types'; + +/** + * Custom ValidationError class for field validation errors + */ +export class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + } +} + +/** + * 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 errorDetails = parseErrorDetails(err); + + const response: ErrorResponse = createErrorResponse( + errorDetails.message, + errorDetails.code, + errorDetails.details + ); + + // Send the error response + res.status(errorDetails.statusCode).json(response); +} + +/** + * 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 + */ +/** + * Internal helper function to parse error details and determine HTTP status codes + */ +function parseErrorDetails(err: Error): { + message: string; + code: string; + details?: any; + statusCode: number; +} { + // MongoDB specific error handling + if (err instanceof MongoError) { + 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 + }; + } + } + + // Validation errors + if (err.name === 'ValidationError') { + return { + message: 'Validation failed', + code: 'VALIDATION_ERROR', + details: err.message, + statusCode: 400 + }; + } + + // Default error handling + return { + message: err.message || 'Internal server error', + code: 'INTERNAL_ERROR', + statusCode: 500 + }; +} + + + +/** + * 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) => { + try { + fn(req, res, next).catch(next); + } catch (error) { + next(error); + } + }; +} + +/** + * 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: T, message?: string): SuccessResponse { + return { + success: true, + message: message || 'Operation completed successfully', + data, + timestamp: new Date().toISOString() + }; +} + +/** + * 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 ValidationError if any required fields are missing + */ +export function validateRequiredFields(body: any, requiredFields: string[]): void { + const missingFields = requiredFields.filter(field => + body[field] == null || body[field] === '' + ); + + if (missingFields.length > 0) { + throw new ValidationError(`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