-
Notifications
You must be signed in to change notification settings - Fork 6
feat: set up repo structure and backend connection #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # MongoDB Configuration | ||
| # Replace with your MongoDB Atlas connection string | ||
| MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/sample_mflix?retryWrites=true&w=majority | ||
|
|
||
| # Server Configuration | ||
| PORT=3001 | ||
| NODE_ENV=development | ||
|
|
||
| # CORS Configuration (frontend URL) | ||
| CORS_ORIGIN=http://localhost:3000 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.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" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want any validation for required vars?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a ticket to verify requirements later in the epic where we can implement this (and anything else that comes up between now and then): https://jira.mongodb.org/browse/DOCSP-54423 |
||
|
|
||
| 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...'); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe mention in a comment or in the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is something we can add to the README once we create that (there's a separate ticket for it for after development) |
||
|
|
||
| // 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want to close the connection before exiting?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a bad idea - added a call to closeDatabaseConnection() |
||
| * 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(); | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,123 @@ | ||||||||||
| /** | ||||||||||
| * 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<Db> { | ||||||||||
| // 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); | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are there any recommended cxn options that we want to show?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely open to adding some if we think its worthwhile - my assumption was that those would be handled via the connection string if they wanted to use any |
||||||||||
|
|
||||||||||
| // 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'}` | ||||||||||
| ); | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
why are we wrapping instead of letting the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| let connect$: Promise<Db>; | ||||||||||
| /** | ||||||||||
| * Establishes connection to MongoDB by using the connection string from environment variables | ||||||||||
| * | ||||||||||
| * @returns Promise<Db> - The connected database instance | ||||||||||
| * @throws Error if connection fails or if MONGODB_URI is not provided | ||||||||||
| */ | ||||||||||
| export async function connectToDatabase(): Promise<Db> { | ||||||||||
| // 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<T extends Document>(collectionName: string): Collection<T> { | ||||||||||
| 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<void> { | ||||||||||
| 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<void> { | ||||||||||
| 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<void> { | ||||||||||
| 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.'); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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<void> { | ||||||
| const moviesCollection = getCollection('movies'); | ||||||
|
|
||||||
| try { | ||||||
| // Execute the find operation with all options | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want to mention
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just a placeholder endpoint for now. I'm working on creating the CRUD endpoints now that adds actual logic - I don't have projection in there but it's probably a good idea to add it! |
||||||
| const movies = await moviesCollection | ||||||
| .find({}) | ||||||
| .limit(10) // TODO: Remove temp limit used for testing | ||||||
|
jordan-smith721 marked this conversation as resolved.
|
||||||
| .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'}`); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
why not pass the actual MDB error?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question - simplified |
||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the convention was kebab-case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch