Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .gitignore
Empty file.
10 changes: 10 additions & 0 deletions server/express/.env.example
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
26 changes: 26 additions & 0 deletions server/express/.gitignore
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
29 changes: 29 additions & 0 deletions server/express/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
119 changes: 119 additions & 0 deletions server/express/src/app.ts
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want any validation for required vars?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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...');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe mention in a comment or in the README that you'd probably want to use a logging library in a production app? (vs. console.log and console.error)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to close the connection before exiting?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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...');
closeDatabaseConnection();
process.exit(0);
});

process.on('SIGTERM', () => {
console.log('\nReceived SIGTERM. Shutting down...');
closeDatabaseConnection();
process.exit(0);
});

// Start the server
startServer();
121 changes: 121 additions & 0 deletions server/express/src/config/database.ts
Original file line number Diff line number Diff line change
@@ -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<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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any recommended cxn options that we want to show?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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 error;
}
}

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.');
}
}
29 changes: 29 additions & 0 deletions server/express/src/controllers/movieController.ts
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to mention .project(some fields) in a comment or in another example to demonstrate projection / improving performance to only return fields we need?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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
Comment thread
jordan-smith721 marked this conversation as resolved.
.toArray();

// Return successful response
res.json(createSuccessResponse(movies, `Found ${movies.length} movies`));

} catch (error) {
throw error;
}
}
19 changes: 19 additions & 0 deletions server/express/src/routes/movies.ts
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;
Loading