diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 54d1c68..36c307f 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,13 +1,27 @@ +// Express framework gives us fucnctions to creeate web server, handle routes, process HTTP req and send responses import express from 'express'; -import userRoutes from './routes/user_route.ts'; +import userRoutes from './routes/user_route.ts'; // these routes define how to handel requests to /api/users +// Creating express application as a single "app" object, started in server.ts +// This object represents entire web server and will be used to: +// register Middleware, define routes .... const app = express(); + +// Middleware setup +// responsible for parsing incoming JSON request bodies, making req.body usable in controllers app.use(express.json()); +// Defining routes +// a simple route that sends back plain text "Hello" when anyone visits the app in browser app.get('/', (_req, res) => { res.send('Hello'); }); +// Mounts the user routes under the path 'api/users' +// any routes defined in user_route.ts will be accessible with URLs starting with /api/users app.use('/api/users', userRoutes); + +// Export the configured Express app, so that server.ts can import it +// Keeping app setup separate from server startup makes the code modular and testable export default app; diff --git a/shatter-backend/src/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index 7281344..17189c4 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -1,21 +1,50 @@ +// import req and res types for type safety import { Request, Response } from 'express'; +import { User } from '../models/user_model.ts'; // imports user model created with mongoose -let users = [ - { id: 1, name: 'Minh', email: 'minh.le4@ucalgary.ca' } -]; +// controller: GET /api/users +// This function handles GET reqs to /api/users +// It fetches all users from MongoDB and sends them as json -export const getUsers = (_req: Request, res: Response) => { - res.json(users); +export const getUsers = async (_req: Request, res: Response) => { + try { + // retrieves all docs from "users" collection + const users = await User.find().lean(); // .lean() returns plain JS objects instead of Mongoose docs, may change incase we need extra model methods later + res.json(users); // sends list of users back as JSON response + } catch (err) { // log the error if something goes wrong + console.error('GET /api/users error:', err); + res.status(500).json({ error: 'Failed to fetch users' }); + } }; -export const createUser = (req: Request, res: Response) => { - const { name, email } = req.body; - - if (!name || !email) { - return res.status(400).json({ error: 'name and email required' }); - } - const newUser = { id: users.length + 1, name, email }; - users.push(newUser); - res.status(201).json(newUser); +// controller: POST /api/users +// reads data from req body, vailidates it and creates a new user +export const createUser = async (req: Request, res: Response) => { + try { + // Destructure the req body sent by the client + // The ?? {} ensures we don't get error if req.body is undefined + const { name, email } = req.body ?? {}; + + // Basic validation to ensure both name and email are provided + // if not respond with bad request and stop further processes + if (!name || !email) { + return res.status(400).json({ error: 'name and email required' }); + } + + // create a new user doc in DB using Mongoose's .create() + const user = await User.create({ name, email }); + // respond with "created" and send back created user as JSON + res.status(201).json(user); + } catch(err: any) { + // Handle duplicate email error + // Mongo DB rejects duplicat value since we have email marked as 'unique' + if (err?.code === 11000) { + return res.status(409).json({ error: 'email already exists' }); + } + + // for all other errors, log them and return generic 500 response + console.error('POST /api/users error:', err); + res.status(500).json({error: 'Failed to create user' }); + } }; diff --git a/shatter-backend/src/models/.gitkeep b/shatter-backend/src/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/shatter-backend/src/models/user_model.ts b/shatter-backend/src/models/user_model.ts new file mode 100644 index 0000000..baf8986 --- /dev/null +++ b/shatter-backend/src/models/user_model.ts @@ -0,0 +1,47 @@ +// Import Schema and model from the Mongoose library. +// - Schema: defines the structure and rules for documents in a collection (like a blueprint). +// - model: creates a model (class) that we use in code to read/write those documents. +import { Schema, model } from 'mongoose'; + +// define TS interface for type safety +// This helps IDE and compiler know what fields exist on a User + +export interface IUser { + name: string; + email: string; +} + +// Create the Mongoose Schema (the database blueprint) + +// A Schema tells Mongoose what fields each document should have +// and what rules apply to those fields. +const UserSchema = new Schema( + { + name: { + type: String, + required: true, // field is mandatory; Mongoose will throw error if missing + trim: true // removes extra space at start and end + }, + email: { + type: String, + required: true, + trim: true, + lowercase: true, // converts all emails to lowercase before saving for consistency + unique: true // enforce uniqueness, error 11000 if duplicate is detected + } + }, + { + // timestamps: true automatically adds two fields to each document: + // - createdAt: Date when the document was first created + // - updatedAt: Date when the document was last modified + timestamps: true + } +); + + +// create and export mongoose model +// model is simply a wrapper around schema that gives access to MongoDB opeprations + +// "User" is the model name +// Mongoose will automatically use "users" as the collection name in MongoDB +export const User = model('User', UserSchema); diff --git a/shatter-backend/src/routes/user_route.ts b/shatter-backend/src/routes/user_route.ts index 6eb63b7..c26b3e8 100644 --- a/shatter-backend/src/routes/user_route.ts +++ b/shatter-backend/src/routes/user_route.ts @@ -1,9 +1,16 @@ +// router is like a mini-express app that allows grouping of related routes together import { Router } from 'express'; import { getUsers, createUser } from '../controllers/user_controller.ts'; +// Importing controller functions that handle logic for each route +// These function define what happens when a req is received +// creating new router instance const router = Router(); -router.get('/', getUsers); -router.post('/', createUser); +// Defining routes for the /api/users path +router.get('/', getUsers); // when GET req is made, run getUsers func +router.post('/', createUser); // when POST req is made, run creatUser func -export default router; \ No newline at end of file +// Export the router so it can be used in app.ts +// app.ts imports router and mounts it under '/api/users' +export default router; diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index 9fbc9b3..c23c38c 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -1,7 +1,32 @@ -import app from './app.ts'; +// This is the main entry point that starts the application -const PORT = 4000; +import 'dotenv/config'; // loads .env file and populates process.env (the node.js object) +import mongoose from 'mongoose'; // mongoose is an Object Data Modelling (ODM) livrary for MongoDB +import app from './app.ts'; // Import the express app -app.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); -}); + +// config + +const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; // use defined PORT in .env if present , otherwise default to 400 +const MONGODB_URI = process.env.MONGO_URI; // read the mongoDB connection from env variables + +// Start up function +async function start() { + try { + if (!MONGODB_URI) { // check that MOGO URI is provided in .env + throw new Error('MONGODB_URI is not set'); + } + await mongoose.connect(MONGODB_URI); // this returns a promise, so we 'await' until it's successfully connected + console.log('Successfully connected to MongoDB'); + + // start listening for incoming HTTP requests on chosen port + app.listen(PORT, () => { + console.log('Server running on http://localhost:${PORT}'); + }); + } catch (err) { // if connection goes wrong, log the error + console.error('Failed to start server:', err); + process.exit(1); // code 1 indicates failure + } +} + +start();