From 620c6edfb80db70feff492ccc21f5cc8114f6dfa Mon Sep 17 00:00:00 2001 From: Kausyar-coder Date: Thu, 22 Jan 2026 17:07:12 +0100 Subject: [PATCH 1/6] Week 1: clean backend structure --- .babelrc => backend/.babelrc | 0 data.json => backend/data.json | 0 package.json => backend/package.json | 16 +++++++++++++--- server.js => backend/server.js | 0 pull_request_template.md | 1 - 5 files changed, 13 insertions(+), 4 deletions(-) rename .babelrc => backend/.babelrc (100%) rename data.json => backend/data.json (100%) rename package.json => backend/package.json (50%) rename server.js => backend/server.js (100%) delete mode 100644 pull_request_template.md diff --git a/.babelrc b/backend/.babelrc similarity index 100% rename from .babelrc rename to backend/.babelrc diff --git a/data.json b/backend/data.json similarity index 100% rename from data.json rename to backend/data.json diff --git a/package.json b/backend/package.json similarity index 50% rename from package.json rename to backend/package.json index bf25bb68..9e935a6e 100644 --- a/package.json +++ b/backend/package.json @@ -1,13 +1,23 @@ { - "name": "project-api", + "name": "kausar", "version": "1.0.0", "description": "Project API", + "homepage": "https://github.com/KausarShangareeva/js-project-api-express.js#readme", + "bugs": { + "url": "https://github.com/KausarShangareeva/js-project-api-express.js/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/KausarShangareeva/js-project-api-express.js.git" + }, + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "server.js", "scripts": { "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" }, - "author": "", - "license": "ISC", "dependencies": { "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", diff --git a/server.js b/backend/server.js similarity index 100% rename from server.js rename to backend/server.js diff --git a/pull_request_template.md b/pull_request_template.md deleted file mode 100644 index fb9fdc30..00000000 --- a/pull_request_template.md +++ /dev/null @@ -1 +0,0 @@ -Please include your Render link here. \ No newline at end of file From 2708aaacc8dd4f93ac30f875474926a9976e6a41 Mon Sep 17 00:00:00 2001 From: Kausyar-coder Date: Thu, 22 Jan 2026 19:20:02 +0100 Subject: [PATCH 2/6] Week 1: Step 4 GET routes completed --- backend/data.json | 244 ++++++++++++++++++++++--------------------- backend/package.json | 8 +- backend/server.js | 55 +++++++--- 3 files changed, 168 insertions(+), 139 deletions(-) diff --git a/backend/data.json b/backend/data.json index a2c844ff..c8db29e2 100644 --- a/backend/data.json +++ b/backend/data.json @@ -1,121 +1,123 @@ -[ - { - "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", - "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 - }, - { - "_id": "682e53cc4fddf50010bbe739", - "message": "My family!", - "hearts": 0, - "createdAt": "2025-05-22T22:29:32.232Z", - "__v": 0 - }, - { - "_id": "682e4f844fddf50010bbe738", - "message": "The smell of coffee in the morning....", - "hearts": 23, - "createdAt": "2025-05-22T22:11:16.075Z", - "__v": 0 - }, - { - "_id": "682e48bf4fddf50010bbe737", - "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", - "hearts": 6, - "createdAt": "2025-05-21T21:42:23.862Z", - "__v": 0 - }, - { - "_id": "682e45804fddf50010bbe736", - "message": "I am happy that I feel healthy and have energy again", - "hearts": 13, - "createdAt": "2025-05-21T21:28:32.196Z", - "__v": 0 - }, - { - "_id": "682e23fecf615800105107aa", - "message": "cold beer", - "hearts": 2, - "createdAt": "2025-05-21T19:05:34.113Z", - "__v": 0 - }, - { - "_id": "682e22aecf615800105107a9", - "message": "My friend is visiting this weekend! <3", - "hearts": 6, - "createdAt": "2025-05-21T18:59:58.121Z", - "__v": 0 - }, - { - "_id": "682cec1b17487d0010a298b6", - "message": "A god joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!", - "hearts": 12, - "createdAt": "2025-05-20T20:54:51.082Z", - "__v": 0 - }, - { - "_id": "682cebbe17487d0010a298b5", - "message": "Tacos and tequila🌮🍹", - "hearts": 2, - "createdAt": "2025-05-19T20:53:18.899Z", - "__v": 0 - }, - { - "_id": "682ceb5617487d0010a298b4", - "message": "Netflix and late night ice-cream🍦", - "hearts": 1, - "createdAt": "2025-05-18T20:51:34.494Z", - "__v": 0 - }, - { - "_id": "682c99ba3bff2d0010f5d44e", - "message": "Summer is coming...", - "hearts": 2, - "createdAt": "2025-05-20T15:03:22.379Z", - "__v": 0 - }, - { - "_id": "682c706c951f7a0017130024", - "message": "Exercise? I thought you said extra fries! 🍟😂", - "hearts": 14, - "createdAt": "2025-05-20T12:07:08.185Z", - "__v": 0 - }, - { - "_id": "682c6fe1951f7a0017130023", - "message": "I’m on a seafood diet. I see food, and I eat it.", - "hearts": 4, - "createdAt": "2025-05-20T12:04:49.978Z", - "__v": 0 - }, - { - "_id": "682c6f0e951f7a0017130022", - "message": "Cute monkeys🐒", - "hearts": 2, - "createdAt": "2025-05-20T12:01:18.308Z", - "__v": 0 - }, - { - "_id": "682c6e65951f7a0017130021", - "message": "The weather is nice!", - "hearts": 0, - "createdAt": "2025-05-20T11:58:29.662Z", - "__v": 0 - }, - { - "_id": "682bfdb4270ca300105af221", - "message": "good vibes and good things", - "hearts": 3, - "createdAt": "2025-05-20T03:57:40.322Z", - "__v": 0 - }, - { - "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", - "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 - } -] \ No newline at end of file +{ + "thoughts": [ + { + "_id": "682bab8c12155b00101732ce", + "message": "Berlin baby", + "hearts": 37, + "createdAt": "2025-05-19T22:07:08.999Z", + "__v": 0 + }, + { + "_id": "682e53cc4fddf50010bbe739", + "message": "My family!", + "hearts": 0, + "createdAt": "2025-05-22T22:29:32.232Z", + "__v": 0 + }, + { + "_id": "682e4f844fddf50010bbe738", + "message": "The smell of coffee in the morning....", + "hearts": 23, + "createdAt": "2025-05-22T22:11:16.075Z", + "__v": 0 + }, + { + "_id": "682e48bf4fddf50010bbe737", + "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", + "hearts": 6, + "createdAt": "2025-05-21T21:42:23.862Z", + "__v": 0 + }, + { + "_id": "682e45804fddf50010bbe736", + "message": "I am happy that I feel healthy and have energy again", + "hearts": 13, + "createdAt": "2025-05-21T21:28:32.196Z", + "__v": 0 + }, + { + "_id": "682e23fecf615800105107aa", + "message": "cold beer", + "hearts": 2, + "createdAt": "2025-05-21T19:05:34.113Z", + "__v": 0 + }, + { + "_id": "682e22aecf615800105107a9", + "message": "My friend is visiting this weekend! <3", + "hearts": 6, + "createdAt": "2025-05-21T18:59:58.121Z", + "__v": 0 + }, + { + "_id": "682cec1b17487d0010a298b6", + "message": "A god joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!", + "hearts": 12, + "createdAt": "2025-05-20T20:54:51.082Z", + "__v": 0 + }, + { + "_id": "682cebbe17487d0010a298b5", + "message": "Tacos and tequila🌮🍹", + "hearts": 2, + "createdAt": "2025-05-19T20:53:18.899Z", + "__v": 0 + }, + { + "_id": "682ceb5617487d0010a298b4", + "message": "Netflix and late night ice-cream🍦", + "hearts": 1, + "createdAt": "2025-05-18T20:51:34.494Z", + "__v": 0 + }, + { + "_id": "682c99ba3bff2d0010f5d44e", + "message": "Summer is coming...", + "hearts": 2, + "createdAt": "2025-05-20T15:03:22.379Z", + "__v": 0 + }, + { + "_id": "682c706c951f7a0017130024", + "message": "Exercise? I thought you said extra fries! 🍟😂", + "hearts": 14, + "createdAt": "2025-05-20T12:07:08.185Z", + "__v": 0 + }, + { + "_id": "682c6fe1951f7a0017130023", + "message": "I’m on a seafood diet. I see food, and I eat it.", + "hearts": 4, + "createdAt": "2025-05-20T12:04:49.978Z", + "__v": 0 + }, + { + "_id": "682c6f0e951f7a0017130022", + "message": "Cute monkeys🐒", + "hearts": 2, + "createdAt": "2025-05-20T12:01:18.308Z", + "__v": 0 + }, + { + "_id": "682c6e65951f7a0017130021", + "message": "The weather is nice!", + "hearts": 0, + "createdAt": "2025-05-20T11:58:29.662Z", + "__v": 0 + }, + { + "_id": "682bfdb4270ca300105af221", + "message": "good vibes and good things", + "hearts": 3, + "createdAt": "2025-05-20T03:57:40.322Z", + "__v": 0 + }, + { + "_id": "682bab8c12155b00101732ce", + "message": "Berlin baby", + "hearts": 37, + "createdAt": "2025-05-19T22:07:08.999Z", + "__v": 0 + } + ] +} diff --git a/backend/package.json b/backend/package.json index 9e935a6e..31ce7cc6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,11 +19,11 @@ "dev": "nodemon server.js --exec babel-node" }, "dependencies": { - "@babel/core": "^7.17.9", - "@babel/node": "^7.16.8", - "@babel/preset-env": "^7.16.11", + "@babel/core": "^7.28.6", + "@babel/node": "^7.28.6", + "@babel/preset-env": "^7.28.6", "cors": "^2.8.5", "express": "^4.17.3", - "nodemon": "^3.0.1" + "nodemon": "^3.1.11" } } diff --git a/backend/server.js b/backend/server.js index f47771bd..f945c5ac 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,22 +1,49 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import data from "./data.json"; -// Defines the port the app will run on. Defaults to 8080, but can be overridden -// when starting the server. Example command to overwrite PORT env variable value: -// PORT=9000 npm start -const port = process.env.PORT || 8080 -const app = express() +const port = process.env.PORT || 3000; +const app = express(); // Add middlewares to enable cors and json body parsing -app.use(cors()) -app.use(express.json()) +app.use(cors()); +app.use(express.json()); -// Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + res.json({ + name: "Happy Thoughts API", + endpoints: [ + { method: "GET", path: "/", description: "API docs" }, + { method: "GET", path: "/thoughts", description: "Get all thoughts" }, + { + method: "GET", + path: "/thoughts/:id", + description: "Get single thought by id", + }, + ], + }); +}); + +app.get("/thoughts", (req, res) => { + res.send(data.thoughts); +}); + +app.get("/thoughts/:id", (req, res) => { + const id = req.params.id; + + const thought = data.thought.find((t) => t._id === id); + + if (!thought) { + return res.status(404).json({ + message: "not found", + id, + }); + } + + res.json(thought); +}); // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +}); From 09140c462e88991bad20248edc7dda8064a31820 Mon Sep 17 00:00:00 2001 From: Kausyar-coder Date: Sun, 8 Feb 2026 20:07:09 +0100 Subject: [PATCH 3/6] Week 2: Add MongoDB with Mongoose and full CRUD endpoints --- README.md | 45 +++++++++++-- backend/package.json | 4 +- backend/server.js | 154 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 175 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0f9f073d..696e2598 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,46 @@ -# Project API +# Happy Thoughts API -This project includes the packages and babel setup for an express server, and is just meant to make things a little simpler to get up and running with. +Hi there! I built this Happy Thoughts API as part of my Technigo JavaScript Bootcamp 2025 journey. This is a RESTful API built with Express.js and MongoDB that lets users create, read, update, and delete happy thoughts, as well as like them. -## Getting started +## Key Features -Install dependencies with `npm install`, then start the server by running `npm run dev` +- Full CRUD operations: Create, Read, Update, and Delete thoughts +- Like (heart) a thought +- Data stored in MongoDB with Mongoose models +- Input validation (message must be 5-140 characters) +- Error handling with proper HTTP status codes +- Database seeding with sample data + +## Tech Stack + +- Node.js +- Express.js +- MongoDB + Mongoose +- dotenv + +## API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/` | API documentation | +| GET | `/thoughts` | Get all thoughts (newest first, limit 20) | +| GET | `/thoughts/:id` | Get a single thought by ID | +| POST | `/thoughts` | Create a new thought | +| PATCH | `/thoughts/:id` | Update a thought | +| DELETE | `/thoughts/:id` | Delete a thought | +| POST | `/thoughts/:id/like` | Like a thought (+1 heart) | + +## Getting Started + +1. Install dependencies: `npm install` +2. Create a `.env` file with your MongoDB connection string: + ``` + MONGO_URL=mongodb+srv://your-connection-string + RESET_DB=true + ``` +3. Start the server: `npm run dev` +4. After seeding, set `RESET_DB=false` in `.env` ## View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +Backend: _add your Render link here_ diff --git a/backend/package.json b/backend/package.json index 31ce7cc6..20fb52c6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,8 +22,10 @@ "@babel/core": "^7.28.6", "@babel/node": "^7.28.6", "@babel/preset-env": "^7.28.6", - "cors": "^2.8.5", + "cors": "^2.8.6", + "dotenv": "^17.2.4", "express": "^4.17.3", + "mongoose": "^9.1.5", "nodemon": "^3.1.11" } } diff --git a/backend/server.js b/backend/server.js index f945c5ac..2cec814f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,49 +1,159 @@ import cors from "cors"; import express from "express"; -import data from "./data.json"; +import mongoose from "mongoose"; +import dotenv from "dotenv"; -const port = process.env.PORT || 3000; -const app = express(); +dotenv.config(); -// Add middlewares to enable cors and json body parsing +const app = express(); app.use(cors()); app.use(express.json()); +// Connect to MongoDB +const mongoURL = process.env.MONGO_URL || "mongodb://localhost/happythoughts"; +mongoose.connect(mongoURL); +mongoose.Promise = Promise; + +// --- Thought Model --- +const Thought = mongoose.model("Thought", { + message: { + type: String, + required: [true, "Message is required"], + minlength: [5, "Message must be at least 5 characters"], + maxlength: [140, "Message must be at most 140 characters"], + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +// --- Seed database --- +// Set RESET_DB=true in .env to seed on startup +if (process.env.RESET_DB === "true") { + const seedDB = async () => { + await Thought.deleteMany(); + const thoughts = [ + { message: "Berlin baby", hearts: 37 }, + { message: "My family!", hearts: 0 }, + { message: "The smell of coffee in the morning....", hearts: 23 }, + { message: "Summer is coming...", hearts: 2 }, + { message: "Cute monkeys", hearts: 2 }, + { message: "The weather is nice!", hearts: 0 }, + { message: "Netflix and late night ice-cream", hearts: 1 }, + { message: "good vibes and good things", hearts: 3 }, + { message: "cold beer", hearts: 2 }, + { message: "I am happy that I feel healthy and have energy again", hearts: 13 }, + ]; + await Thought.insertMany(thoughts); + console.log("Database seeded!"); + }; + seedDB(); +} + +// --- Routes --- + +// GET / - API documentation app.get("/", (req, res) => { res.json({ name: "Happy Thoughts API", endpoints: [ - { method: "GET", path: "/", description: "API docs" }, { method: "GET", path: "/thoughts", description: "Get all thoughts" }, - { - method: "GET", - path: "/thoughts/:id", - description: "Get single thought by id", - }, + { method: "GET", path: "/thoughts/:id", description: "Get one thought" }, + { method: "POST", path: "/thoughts", description: "Create a thought" }, + { method: "PATCH", path: "/thoughts/:id", description: "Update a thought" }, + { method: "DELETE", path: "/thoughts/:id", description: "Delete a thought" }, + { method: "POST", path: "/thoughts/:id/like", description: "Like a thought" }, ], }); }); -app.get("/thoughts", (req, res) => { - res.send(data.thoughts); +// GET /thoughts - get all thoughts (newest first) +app.get("/thoughts", async (req, res) => { + try { + const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); + res.json(thoughts); + } catch (err) { + res.status(400).json({ error: "Could not get thoughts" }); + } }); -app.get("/thoughts/:id", (req, res) => { - const id = req.params.id; +// GET /thoughts/:id - get one thought +app.get("/thoughts/:id", async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.json(thought); + } catch (err) { + res.status(400).json({ error: "Invalid id" }); + } +}); - const thought = data.thought.find((t) => t._id === id); +// POST /thoughts - create a new thought +app.post("/thoughts", async (req, res) => { + try { + const thought = await new Thought({ message: req.body.message }).save(); + res.status(201).json(thought); + } catch (err) { + res.status(400).json({ error: "Could not save thought", details: err.message }); + } +}); - if (!thought) { - return res.status(404).json({ - message: "not found", - id, - }); +// PATCH /thoughts/:id - update a thought +app.patch("/thoughts/:id", async (req, res) => { + try { + const thought = await Thought.findByIdAndUpdate( + req.params.id, + { message: req.body.message }, + { new: true, runValidators: true } + ); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.json(thought); + } catch (err) { + res.status(400).json({ error: "Could not update thought", details: err.message }); } +}); - res.json(thought); +// DELETE /thoughts/:id - delete a thought +app.delete("/thoughts/:id", async (req, res) => { + try { + const thought = await Thought.findByIdAndDelete(req.params.id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.json(thought); + } catch (err) { + res.status(400).json({ error: "Could not delete thought" }); + } }); -// Start the server +// POST /thoughts/:id/like - add a heart to a thought +app.post("/thoughts/:id/like", async (req, res) => { + try { + const thought = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, + { new: true } + ); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.json(thought); + } catch (err) { + res.status(400).json({ error: "Could not like thought" }); + } +}); + +// Start server +const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); From 4447f408f5fecf50feba958fc565991245572c78 Mon Sep 17 00:00:00 2001 From: Kausyar-coder Date: Sun, 8 Feb 2026 20:28:22 +0100 Subject: [PATCH 4/6] Week 3: Add user authentication with bcrypt --- backend/package.json | 1 + backend/server.js | 163 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 133 insertions(+), 31 deletions(-) diff --git a/backend/package.json b/backend/package.json index 20fb52c6..72d40459 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,6 +22,7 @@ "@babel/core": "^7.28.6", "@babel/node": "^7.28.6", "@babel/preset-env": "^7.28.6", + "bcrypt": "^6.0.0", "cors": "^2.8.6", "dotenv": "^17.2.4", "express": "^4.17.3", diff --git a/backend/server.js b/backend/server.js index 2cec814f..c11fa86e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,7 @@ import cors from "cors"; import express from "express"; import mongoose from "mongoose"; +import bcrypt from "bcrypt"; import dotenv from "dotenv"; dotenv.config(); @@ -14,6 +15,30 @@ const mongoURL = process.env.MONGO_URL || "mongodb://localhost/happythoughts"; mongoose.connect(mongoURL); mongoose.Promise = Promise; +// --- User Model --- +const User = mongoose.model("User", { + name: { + type: String, + required: [true, "Name is required"], + minlength: [2, "Name must be at least 2 characters"], + }, + email: { + type: String, + required: [true, "Email is required"], + unique: true, + match: [/.+@.+\..+/, "Please enter a valid email"], + }, + password: { + type: String, + required: [true, "Password is required"], + minlength: [6, "Password must be at least 6 characters"], + }, + accessToken: { + type: String, + default: () => bcrypt.genSaltSync(), + }, +}); + // --- Thought Model --- const Thought = mongoose.model("Thought", { message: { @@ -30,24 +55,41 @@ const Thought = mongoose.model("Thought", { type: Date, default: Date.now, }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, }); +// --- Auth Middleware --- +const auth = async (req, res, next) => { + const token = req.header("Authorization"); + if (!token) { + return res.status(401).json({ error: "Not logged in" }); + } + const user = await User.findOne({ accessToken: token }); + if (!user) { + return res.status(401).json({ error: "Invalid token" }); + } + req.user = user; + next(); +}; + // --- Seed database --- -// Set RESET_DB=true in .env to seed on startup if (process.env.RESET_DB === "true") { const seedDB = async () => { await Thought.deleteMany(); const thoughts = [ - { message: "Berlin baby", hearts: 37 }, - { message: "My family!", hearts: 0 }, - { message: "The smell of coffee in the morning....", hearts: 23 }, - { message: "Summer is coming...", hearts: 2 }, - { message: "Cute monkeys", hearts: 2 }, - { message: "The weather is nice!", hearts: 0 }, - { message: "Netflix and late night ice-cream", hearts: 1 }, - { message: "good vibes and good things", hearts: 3 }, - { message: "cold beer", hearts: 2 }, - { message: "I am happy that I feel healthy and have energy again", hearts: 13 }, + { message: "Code is like humor. When you have to explain it, it's bad.", hearts: 12 }, + { message: "First, solve the problem. Then, write the code.", hearts: 25 }, + { message: "The best error message is the one that never shows up.", hearts: 8 }, + { message: "Talk is cheap. Show me the code.", hearts: 31 }, + { message: "It works on my machine! Then we ship your machine.", hearts: 19 }, + { message: "Simplicity is the soul of efficiency.", hearts: 14 }, + { message: "Make it work, make it right, make it fast.", hearts: 22 }, + { message: "Every great developer you know got there by solving problems they were unqualified to solve.", hearts: 17 }, + { message: "The only way to learn a new programming language is by writing programs in it.", hearts: 9 }, + { message: "Programming is the art of telling another human what one wants the computer to do.", hearts: 11 }, ]; await Thought.insertMany(thoughts); console.log("Database seeded!"); @@ -62,20 +104,69 @@ app.get("/", (req, res) => { res.json({ name: "Happy Thoughts API", endpoints: [ + { method: "POST", path: "/register", description: "Register a new user" }, + { method: "POST", path: "/login", description: "Login" }, { method: "GET", path: "/thoughts", description: "Get all thoughts" }, { method: "GET", path: "/thoughts/:id", description: "Get one thought" }, - { method: "POST", path: "/thoughts", description: "Create a thought" }, - { method: "PATCH", path: "/thoughts/:id", description: "Update a thought" }, - { method: "DELETE", path: "/thoughts/:id", description: "Delete a thought" }, + { method: "POST", path: "/thoughts", description: "Create a thought (auth)" }, + { method: "PATCH", path: "/thoughts/:id", description: "Update a thought (auth)" }, + { method: "DELETE", path: "/thoughts/:id", description: "Delete a thought (auth)" }, { method: "POST", path: "/thoughts/:id/like", description: "Like a thought" }, ], }); }); +// POST /register - create a new user +app.post("/register", async (req, res) => { + try { + const { name, email, password } = req.body; + const existing = await User.findOne({ email }); + if (existing) { + return res.status(400).json({ error: "That email address already exists" }); + } + const hashedPassword = await bcrypt.hash(password, 10); + const user = await new User({ name, email, password: hashedPassword }).save(); + res.status(201).json({ + id: user._id, + name: user.name, + email: user.email, + accessToken: user.accessToken, + }); + } catch (err) { + res.status(400).json({ error: "Could not register", details: err.message }); + } +}); + +// POST /login - login +app.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + const user = await User.findOne({ email }); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + return res.status(401).json({ error: "Wrong password" }); + } + res.json({ + id: user._id, + name: user.name, + email: user.email, + accessToken: user.accessToken, + }); + } catch (err) { + res.status(400).json({ error: "Could not login", details: err.message }); + } +}); + // GET /thoughts - get all thoughts (newest first) app.get("/thoughts", async (req, res) => { try { - const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); + const thoughts = await Thought.find() + .sort({ createdAt: -1 }) + .limit(20) + .populate("user", "name"); res.json(thoughts); } catch (err) { res.status(400).json({ error: "Could not get thoughts" }); @@ -85,7 +176,7 @@ app.get("/thoughts", async (req, res) => { // GET /thoughts/:id - get one thought app.get("/thoughts/:id", async (req, res) => { try { - const thought = await Thought.findById(req.params.id); + const thought = await Thought.findById(req.params.id).populate("user", "name"); if (!thought) { return res.status(404).json({ error: "Thought not found" }); } @@ -95,40 +186,50 @@ app.get("/thoughts/:id", async (req, res) => { } }); -// POST /thoughts - create a new thought -app.post("/thoughts", async (req, res) => { +// POST /thoughts - create a new thought (auth required) +app.post("/thoughts", auth, async (req, res) => { try { - const thought = await new Thought({ message: req.body.message }).save(); - res.status(201).json(thought); + const thought = await new Thought({ + message: req.body.message, + user: req.user._id, + }).save(); + const populated = await thought.populate("user", "name"); + res.status(201).json(populated); } catch (err) { res.status(400).json({ error: "Could not save thought", details: err.message }); } }); -// PATCH /thoughts/:id - update a thought -app.patch("/thoughts/:id", async (req, res) => { +// PATCH /thoughts/:id - update a thought (auth, only owner) +app.patch("/thoughts/:id", auth, async (req, res) => { try { - const thought = await Thought.findByIdAndUpdate( - req.params.id, - { message: req.body.message }, - { new: true, runValidators: true } - ); + const thought = await Thought.findById(req.params.id); if (!thought) { return res.status(404).json({ error: "Thought not found" }); } - res.json(thought); + if (String(thought.user) !== String(req.user._id)) { + return res.status(403).json({ error: "You can only edit your own thoughts" }); + } + thought.message = req.body.message; + await thought.save(); + const populated = await thought.populate("user", "name"); + res.json(populated); } catch (err) { res.status(400).json({ error: "Could not update thought", details: err.message }); } }); -// DELETE /thoughts/:id - delete a thought -app.delete("/thoughts/:id", async (req, res) => { +// DELETE /thoughts/:id - delete a thought (auth, only owner) +app.delete("/thoughts/:id", auth, async (req, res) => { try { - const thought = await Thought.findByIdAndDelete(req.params.id); + const thought = await Thought.findById(req.params.id); if (!thought) { return res.status(404).json({ error: "Thought not found" }); } + if (String(thought.user) !== String(req.user._id)) { + return res.status(403).json({ error: "You can only delete your own thoughts" }); + } + await thought.deleteOne(); res.json(thought); } catch (err) { res.status(400).json({ error: "Could not delete thought" }); From 0ca22263405dd3daced43b76465d39dee936add5 Mon Sep 17 00:00:00 2001 From: Kausyar-coder Date: Tue, 24 Feb 2026 19:54:22 +0100 Subject: [PATCH 5/6] Temp: code formatting and video script Co-Authored-By: Claude Opus 4.6 --- VIDEO_SCRIPT.md | 530 ++++++++++++++++++++++++++++++++++++++++++++++ backend/server.js | 96 +++++++-- 2 files changed, 607 insertions(+), 19 deletions(-) create mode 100644 VIDEO_SCRIPT.md diff --git a/VIDEO_SCRIPT.md b/VIDEO_SCRIPT.md new file mode 100644 index 00000000..c4d4dacb --- /dev/null +++ b/VIDEO_SCRIPT.md @@ -0,0 +1,530 @@ +# Video Script / Текст для видео (8-10 мин) + +--- + +## РУССКАЯ ВЕРСИЯ + +--- + +### 1. Введение (~30 сек) + +Привет! Сегодня я покажу свой бэкенд-проект — Happy Thoughts API. Это REST API, написанный на Node.js с Express и MongoDB. Он позволяет пользователям создавать "мысли", лайкать их, а также регистрироваться и логиниться. Я покажу код, объясню каждую часть, и покажу как я задеплоила проект на Render и подключила к фронтенду. + +--- + +### 2. Обзор package.json (~30 сек) + +Начнём с package.json. Здесь перечислены все библиотеки, которые я использую: + +- **express** — это фреймворк для создания сервера и обработки запросов +- **mongoose** — библиотека для работы с MongoDB (базой данных) +- **cors** — позволяет фронтенду обращаться к нашему API (без этого браузер заблокирует запросы) +- **bcrypt** — для шифрования паролей пользователей +- **dotenv** — для хранения секретных данных (например, ссылка на базу данных) в файле .env +- **nodemon** — автоматически перезапускает сервер при изменении кода (для разработки) +- **babel** — позволяет использовать современный JavaScript (import/export) + +Скрипт `npm run dev` запускает сервер в режиме разработки с nodemon. + +--- + +### 3. Импорты и настройка сервера (~40 сек) + +``` +import cors from "cors"; +import express from "express"; +import mongoose from "mongoose"; +import bcrypt from "bcrypt"; +import dotenv from "dotenv"; +``` + +В начале файла я импортирую все нужные библиотеки. + +Потом: + +``` +dotenv.config(); +``` +Это загружает переменные из файла `.env` — там хранится ссылка на базу данных. + +``` +const app = express(); +app.use(cors()); +app.use(express.json()); +``` + +- `express()` — создаёт наше приложение +- `cors()` — разрешает запросы с других доменов (например, с фронтенда) +- `express.json()` — говорит серверу понимать JSON в теле запросов + +``` +mongoose.connect(mongoURL); +``` +Эта строка подключает нас к MongoDB базе данных. + +--- + +### 4. Модели данных (~1 мин) + +**Модель User (Пользователь):** + +У каждого пользователя есть: +- `name` — имя (обязательное, минимум 2 символа) +- `email` — электронная почта (обязательное, уникальное, проверяется формат) +- `password` — пароль (обязательное, минимум 6 символов, хранится в зашифрованном виде) +- `accessToken` — токен для авторизации (генерируется автоматически) + +**Модель Thought (Мысль):** + +У каждой мысли есть: +- `message` — текст (от 5 до 140 символов) +- `hearts` — количество лайков (по умолчанию 0) +- `createdAt` — дата создания (автоматически) +- `user` — ссылка на пользователя, который создал мысль (это связь между коллекциями) + +--- + +### 5. Middleware для авторизации (~40 сек) + +``` +const auth = async (req, res, next) => { ... } +``` + +Это "прослойка" (middleware), которая проверяет — авторизован ли пользователь. + +Как она работает: +1. Берёт токен из заголовка запроса `Authorization` +2. Если токена нет — возвращает ошибку 401 "Not logged in" +3. Ищет пользователя с таким токеном в базе данных +4. Если не находит — возвращает ошибку 401 "Invalid token" +5. Если нашёл — сохраняет пользователя в `req.user` и пропускает дальше (вызывает `next()`) + +Я использую этот middleware на роутах, где нужна авторизация — создание, редактирование, удаление мыслей. + +--- + +### 6. Seed Database (~30 сек) + +``` +if (process.env.RESET_DB === "true") { ... } +``` + +Эта часть проверяет: если переменная `RESET_DB` равна `true`, то база данных очищается и заполняется 10 примерами мыслей. Это удобно для тестирования — всегда есть начальные данные. + +--- + +### 7. Роуты (endpoints) (~2 мин) + +**GET /** — корневой роут. Возвращает список всех доступных endpoint-ов. Это как документация API. + +**POST /register** — регистрация нового пользователя: +- Принимает имя, email и пароль +- Проверяет, не занят ли email +- Шифрует пароль с помощью bcrypt (пароль никогда не хранится в открытом виде!) +- Сохраняет пользователя и возвращает данные + токен + +**POST /login** — вход: +- Принимает email и пароль +- Ищет пользователя по email +- Сравнивает пароль с зашифрованным (bcrypt.compare) +- Если всё ок — возвращает данные + токен + +**GET /thoughts** — получить все мысли: +- Сортирует от новых к старым (`sort createdAt: -1`) +- Ограничивает до 20 штук (`limit(20)`) +- Подтягивает имя автора (`populate("user", "name")`) + +**GET /thoughts/:id** — получить одну мысль по её ID + +**POST /thoughts** (auth) — создать новую мысль: +- Требует авторизацию (middleware `auth`) +- Берёт текст из body и ID пользователя из `req.user` + +**PATCH /thoughts/:id** (auth) — обновить мысль: +- Требует авторизацию +- Проверяет, что ты автор этой мысли (нельзя редактировать чужие) + +**DELETE /thoughts/:id** (auth) — удалить мысль: +- Требует авторизацию +- Проверяет, что ты автор + +**POST /thoughts/:id/like** — лайкнуть мысль: +- Увеличивает `hearts` на 1 с помощью `$inc` +- Не требует авторизации — любой может лайкнуть + +--- + +### 8. Запуск сервера (~15 сек) + +``` +const port = process.env.PORT || 3000; +app.listen(port, () => { ... }); +``` + +Сервер запускается на порту из переменной окружения (Render сам задаёт PORT) или на порту 3000 при локальной разработке. + +--- + +### 9. Деплой на Render (~1 мин) + +Чтобы задеплоить проект: + +1. Я загрузила код на GitHub +2. Зашла на сайт render.com и создала новый Web Service +3. Подключила свой GitHub репозиторий +4. В настройках указала: + - **Root Directory**: `backend` (потому что мой server.js лежит в папке backend) + - **Build Command**: `npm install` + - **Start Command**: `npm start` +5. В разделе Environment Variables добавила: + - `MONGO_URL` — ссылку на MongoDB Atlas (облачную базу данных) + - `RESET_DB` — `true` (чтобы заполнить базу начальными данными) +6. Нажала Deploy — и через пару минут API стал доступен по ссылке от Render + +Теперь мой API работает онлайн и любой может к нему обращаться! + +--- + +### 10. Подключение к фронтенду Happy Thoughts (~1 мин) + +В моём фронтенд-проекте `js-project-happy-thoughts` я заменила URL API. + +Раньше фронтенд обращался к стандартному API от Technigo. Теперь он обращается к моему API на Render. + +Я просто заменила базовый URL в коде фронтенда, например: + +``` +const API_URL = "https://мой-проект.onrender.com"; +``` + +И теперь: +- Когда пользователь открывает страницу — фронтенд делает GET запрос на `/thoughts` и показывает все мысли +- Когда нажимает кнопку сердечка — отправляет POST на `/thoughts/:id/like` +- Когда пишет новую мысль — отправляет POST на `/thoughts` + +Фронтенд и бэкенд работают вместе, но хостятся отдельно — фронтенд на Netlify, бэкенд на Render. + +--- + +### 11. Тестирование в Postman (~1 мин) + +Postman — это инструмент для тестирования API без фронтенда. + +Примеры: + +1. **GET все мысли**: метод GET, URL: `https://мой-проект.onrender.com/thoughts` — получаем список мыслей + +2. **Регистрация**: метод POST, URL: `/register`, в Body (raw JSON): + ```json + { + "name": "Kausar", + "email": "kausar@test.com", + "password": "123456" + } + ``` + В ответе получаем accessToken — его нужно скопировать + +3. **Создать мысль** (с авторизацией): метод POST, URL: `/thoughts`, + - В Headers добавляем: `Authorization: <ваш_токен>` + - В Body: `{ "message": "Hello from Postman!" }` + +4. **Лайкнуть мысль**: метод POST, URL: `/thoughts//like` + +В Postman удобно тестировать все endpoint-ы, особенно те, которые требуют авторизацию — на фронтенде это сложнее проверить. + +--- + +### 12. MongoDB Compass и Atlas (~1 мин) + +**MongoDB Atlas** — это облачная база данных. Я создала бесплатный кластер (cluster) на mongodb.com. + +Из Atlas я получила строку подключения (connection string), которую использую как `MONGO_URL`. Она выглядит примерно так: +``` +mongodb+srv://username:password@cluster.mongodb.net/happythoughts +``` + +**MongoDB Compass** — это настольное приложение для просмотра базы данных. Я вставляю ту же строку подключения и могу: +- Видеть все коллекции (users, thoughts) +- Просматривать документы — каждая мысль, каждый пользователь +- Видеть зашифрованные пароли (они выглядят как длинная строка) +- Видеть связи между мыслями и пользователями через поле `user` (ObjectId) +- Редактировать или удалять данные вручную (для отладки) + +--- + +### 13. Заключение (~20 сек) + +Итого — я создала полноценный REST API с: +- Полным CRUD (создание, чтение, обновление, удаление) +- Авторизацией пользователей (регистрация, логин, токены) +- Шифрованием паролей +- Связями между моделями +- Деплоем на Render +- Подключением к фронтенду + +Спасибо за внимание! + +--- +--- + +## ENGLISH VERSION + +--- + +### 1. Introduction (~30 sec) + +Hi! Today I'll walk you through my backend project — Happy Thoughts API. It's a REST API built with Node.js, Express, and MongoDB. It allows users to create "thoughts", like them, and also register and log in. I'll go through the code, explain each part, and show how I deployed it to Render and connected it to my frontend. + +--- + +### 2. Package.json overview (~30 sec) + +Let's start with package.json. Here are all the libraries I'm using: + +- **express** — a framework for creating a server and handling requests +- **mongoose** — a library for working with MongoDB (our database) +- **cors** — allows the frontend to make requests to our API (without it, the browser would block requests) +- **bcrypt** — for hashing user passwords +- **dotenv** — for storing secret data (like the database URL) in a .env file +- **nodemon** — automatically restarts the server when code changes (for development) +- **babel** — allows us to use modern JavaScript syntax (import/export) + +The script `npm run dev` starts the server in development mode with nodemon. + +--- + +### 3. Imports and server setup (~40 sec) + +``` +import cors from "cors"; +import express from "express"; +import mongoose from "mongoose"; +import bcrypt from "bcrypt"; +import dotenv from "dotenv"; +``` + +At the top of the file, I import all the libraries I need. + +Then: + +``` +dotenv.config(); +``` +This loads variables from the `.env` file — that's where the database connection string is stored. + +``` +const app = express(); +app.use(cors()); +app.use(express.json()); +``` + +- `express()` — creates our application +- `cors()` — allows requests from other domains (like from our frontend) +- `express.json()` — tells the server to understand JSON in request bodies + +``` +mongoose.connect(mongoURL); +``` +This line connects us to the MongoDB database. + +--- + +### 4. Data Models (~1 min) + +**User Model:** + +Each user has: +- `name` — required, at least 2 characters +- `email` — required, unique, validated with a regex pattern +- `password` — required, at least 6 characters, stored encrypted +- `accessToken` — generated automatically, used for authentication + +**Thought Model:** + +Each thought has: +- `message` — the text (5 to 140 characters) +- `hearts` — number of likes (default 0) +- `createdAt` — creation date (set automatically) +- `user` — a reference to the user who created it (this links the two collections) + +--- + +### 5. Authentication Middleware (~40 sec) + +``` +const auth = async (req, res, next) => { ... } +``` + +This is a middleware — a function that runs before the route handler. + +How it works: +1. It takes the token from the `Authorization` header +2. If there's no token — returns 401 error "Not logged in" +3. Looks for a user with that token in the database +4. If not found — returns 401 "Invalid token" +5. If found — saves the user to `req.user` and calls `next()` to continue + +I use this middleware on routes that require authentication — creating, editing, and deleting thoughts. + +--- + +### 6. Seed Database (~30 sec) + +``` +if (process.env.RESET_DB === "true") { ... } +``` + +This part checks: if the `RESET_DB` environment variable is `true`, it clears the database and fills it with 10 sample thoughts. This is useful for testing — you always have some initial data. + +--- + +### 7. Routes (endpoints) (~2 min) + +**GET /** — the root route. Returns a list of all available endpoints. It works like API documentation. + +**POST /register** — register a new user: +- Accepts name, email, and password +- Checks if the email is already taken +- Hashes the password with bcrypt (passwords are never stored in plain text!) +- Saves the user and returns their data + token + +**POST /login** — log in: +- Accepts email and password +- Finds the user by email +- Compares the password with the hashed one (bcrypt.compare) +- If everything matches — returns user data + token + +**GET /thoughts** — get all thoughts: +- Sorted from newest to oldest (`sort createdAt: -1`) +- Limited to 20 (`limit(20)`) +- Includes the author's name (`populate("user", "name")`) + +**GET /thoughts/:id** — get a single thought by its ID + +**POST /thoughts** (auth) — create a new thought: +- Requires authentication (the `auth` middleware) +- Takes the message from the body and the user ID from `req.user` + +**PATCH /thoughts/:id** (auth) — update a thought: +- Requires authentication +- Checks that you are the author (you can't edit others' thoughts) + +**DELETE /thoughts/:id** (auth) — delete a thought: +- Requires authentication +- Checks that you are the author + +**POST /thoughts/:id/like** — like a thought: +- Increases `hearts` by 1 using `$inc` +- No authentication required — anyone can like + +--- + +### 8. Starting the server (~15 sec) + +``` +const port = process.env.PORT || 3000; +app.listen(port, () => { ... }); +``` + +The server starts on the port from the environment variable (Render sets PORT automatically) or on port 3000 for local development. + +--- + +### 9. Deploying to Render (~1 min) + +To deploy the project: + +1. I pushed the code to GitHub +2. Went to render.com and created a new Web Service +3. Connected my GitHub repository +4. In the settings I specified: + - **Root Directory**: `backend` (because my server.js is inside the backend folder) + - **Build Command**: `npm install` + - **Start Command**: `npm start` +5. In Environment Variables I added: + - `MONGO_URL` — the connection string from MongoDB Atlas (cloud database) + - `RESET_DB` — `true` (to seed the database with initial data) +6. Clicked Deploy — and in a couple of minutes the API was live with a Render URL + +Now my API is online and anyone can access it! + +--- + +### 10. Connecting to Happy Thoughts frontend (~1 min) + +In my frontend project `js-project-happy-thoughts`, I replaced the API URL. + +Before, the frontend was using the default Technigo API. Now it points to my own API on Render. + +I simply changed the base URL in the frontend code, for example: + +``` +const API_URL = "https://my-project.onrender.com"; +``` + +And now: +- When a user opens the page — the frontend makes a GET request to `/thoughts` and displays all thoughts +- When they click the heart button — it sends a POST to `/thoughts/:id/like` +- When they write a new thought — it sends a POST to `/thoughts` + +The frontend and backend work together but are hosted separately — frontend on Netlify, backend on Render. + +--- + +### 11. Testing with Postman (~1 min) + +Postman is a tool for testing APIs without a frontend. + +Examples: + +1. **GET all thoughts**: method GET, URL: `https://my-project.onrender.com/thoughts` — we get a list of thoughts + +2. **Register**: method POST, URL: `/register`, in Body (raw JSON): + ```json + { + "name": "Kausar", + "email": "kausar@test.com", + "password": "123456" + } + ``` + In the response we get an accessToken — copy it + +3. **Create a thought** (with auth): method POST, URL: `/thoughts`, + - In Headers add: `Authorization: ` + - In Body: `{ "message": "Hello from Postman!" }` + +4. **Like a thought**: method POST, URL: `/thoughts//like` + +Postman is great for testing all endpoints, especially the ones that require authentication — it's harder to test those from the frontend. + +--- + +### 12. MongoDB Compass and Atlas (~1 min) + +**MongoDB Atlas** — is a cloud database. I created a free cluster on mongodb.com. + +From Atlas I got a connection string, which I use as `MONGO_URL`. It looks something like: +``` +mongodb+srv://username:password@cluster.mongodb.net/happythoughts +``` + +**MongoDB Compass** — is a desktop app for viewing the database. I paste the same connection string and I can: +- See all collections (users, thoughts) +- Browse documents — each thought, each user +- See the encrypted passwords (they look like a long string) +- See the relationships between thoughts and users through the `user` field (ObjectId) +- Edit or delete data manually (for debugging) + +--- + +### 13. Conclusion (~20 sec) + +To sum up — I built a full REST API with: +- Full CRUD (Create, Read, Update, Delete) +- User authentication (register, login, tokens) +- Password hashing +- Model relationships +- Deployment on Render +- Connection to a frontend + +Thanks for watching! diff --git a/backend/server.js b/backend/server.js index c11fa86e..fee51f5f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -12,6 +12,7 @@ app.use(express.json()); // Connect to MongoDB const mongoURL = process.env.MONGO_URL || "mongodb://localhost/happythoughts"; + mongoose.connect(mongoURL); mongoose.Promise = Promise; @@ -80,16 +81,40 @@ if (process.env.RESET_DB === "true") { const seedDB = async () => { await Thought.deleteMany(); const thoughts = [ - { message: "Code is like humor. When you have to explain it, it's bad.", hearts: 12 }, - { message: "First, solve the problem. Then, write the code.", hearts: 25 }, - { message: "The best error message is the one that never shows up.", hearts: 8 }, + { + message: "Code is like humor. When you have to explain it, it's bad.", + hearts: 12, + }, + { + message: "First, solve the problem. Then, write the code.", + hearts: 25, + }, + { + message: "The best error message is the one that never shows up.", + hearts: 8, + }, { message: "Talk is cheap. Show me the code.", hearts: 31 }, - { message: "It works on my machine! Then we ship your machine.", hearts: 19 }, + { + message: "It works on my machine! Then we ship your machine.", + hearts: 19, + }, { message: "Simplicity is the soul of efficiency.", hearts: 14 }, { message: "Make it work, make it right, make it fast.", hearts: 22 }, - { message: "Every great developer you know got there by solving problems they were unqualified to solve.", hearts: 17 }, - { message: "The only way to learn a new programming language is by writing programs in it.", hearts: 9 }, - { message: "Programming is the art of telling another human what one wants the computer to do.", hearts: 11 }, + { + message: + "Every great developer you know got there by solving problems they were unqualified to solve.", + hearts: 17, + }, + { + message: + "The only way to learn a new programming language is by writing programs in it.", + hearts: 9, + }, + { + message: + "Programming is the art of telling another human what one wants the computer to do.", + hearts: 11, + }, ]; await Thought.insertMany(thoughts); console.log("Database seeded!"); @@ -108,10 +133,26 @@ app.get("/", (req, res) => { { method: "POST", path: "/login", description: "Login" }, { method: "GET", path: "/thoughts", description: "Get all thoughts" }, { method: "GET", path: "/thoughts/:id", description: "Get one thought" }, - { method: "POST", path: "/thoughts", description: "Create a thought (auth)" }, - { method: "PATCH", path: "/thoughts/:id", description: "Update a thought (auth)" }, - { method: "DELETE", path: "/thoughts/:id", description: "Delete a thought (auth)" }, - { method: "POST", path: "/thoughts/:id/like", description: "Like a thought" }, + { + method: "POST", + path: "/thoughts", + description: "Create a thought (auth)", + }, + { + method: "PATCH", + path: "/thoughts/:id", + description: "Update a thought (auth)", + }, + { + method: "DELETE", + path: "/thoughts/:id", + description: "Delete a thought (auth)", + }, + { + method: "POST", + path: "/thoughts/:id/like", + description: "Like a thought", + }, ], }); }); @@ -122,10 +163,16 @@ app.post("/register", async (req, res) => { const { name, email, password } = req.body; const existing = await User.findOne({ email }); if (existing) { - return res.status(400).json({ error: "That email address already exists" }); + return res + .status(400) + .json({ error: "That email address already exists" }); } const hashedPassword = await bcrypt.hash(password, 10); - const user = await new User({ name, email, password: hashedPassword }).save(); + const user = await new User({ + name, + email, + password: hashedPassword, + }).save(); res.status(201).json({ id: user._id, name: user.name, @@ -176,7 +223,10 @@ app.get("/thoughts", async (req, res) => { // GET /thoughts/:id - get one thought app.get("/thoughts/:id", async (req, res) => { try { - const thought = await Thought.findById(req.params.id).populate("user", "name"); + const thought = await Thought.findById(req.params.id).populate( + "user", + "name", + ); if (!thought) { return res.status(404).json({ error: "Thought not found" }); } @@ -196,7 +246,9 @@ app.post("/thoughts", auth, async (req, res) => { const populated = await thought.populate("user", "name"); res.status(201).json(populated); } catch (err) { - res.status(400).json({ error: "Could not save thought", details: err.message }); + res + .status(400) + .json({ error: "Could not save thought", details: err.message }); } }); @@ -208,14 +260,18 @@ app.patch("/thoughts/:id", auth, async (req, res) => { return res.status(404).json({ error: "Thought not found" }); } if (String(thought.user) !== String(req.user._id)) { - return res.status(403).json({ error: "You can only edit your own thoughts" }); + return res + .status(403) + .json({ error: "You can only edit your own thoughts" }); } thought.message = req.body.message; await thought.save(); const populated = await thought.populate("user", "name"); res.json(populated); } catch (err) { - res.status(400).json({ error: "Could not update thought", details: err.message }); + res + .status(400) + .json({ error: "Could not update thought", details: err.message }); } }); @@ -227,7 +283,9 @@ app.delete("/thoughts/:id", auth, async (req, res) => { return res.status(404).json({ error: "Thought not found" }); } if (String(thought.user) !== String(req.user._id)) { - return res.status(403).json({ error: "You can only delete your own thoughts" }); + return res + .status(403) + .json({ error: "You can only delete your own thoughts" }); } await thought.deleteOne(); res.json(thought); @@ -242,7 +300,7 @@ app.post("/thoughts/:id/like", async (req, res) => { const thought = await Thought.findByIdAndUpdate( req.params.id, { $inc: { hearts: 1 } }, - { new: true } + { new: true }, ); if (!thought) { return res.status(404).json({ error: "Thought not found" }); From 0a60cc31e6b10bd6ee0f4e6c73803e1cdfba032c Mon Sep 17 00:00:00 2001 From: Kausyar-coder Date: Tue, 24 Feb 2026 20:48:28 +0100 Subject: [PATCH 6/6] Remove VIDEO_SCRIPT.md Co-Authored-By: Claude Opus 4.6 --- VIDEO_SCRIPT.md | 530 ------------------------------------------------ 1 file changed, 530 deletions(-) delete mode 100644 VIDEO_SCRIPT.md diff --git a/VIDEO_SCRIPT.md b/VIDEO_SCRIPT.md deleted file mode 100644 index c4d4dacb..00000000 --- a/VIDEO_SCRIPT.md +++ /dev/null @@ -1,530 +0,0 @@ -# Video Script / Текст для видео (8-10 мин) - ---- - -## РУССКАЯ ВЕРСИЯ - ---- - -### 1. Введение (~30 сек) - -Привет! Сегодня я покажу свой бэкенд-проект — Happy Thoughts API. Это REST API, написанный на Node.js с Express и MongoDB. Он позволяет пользователям создавать "мысли", лайкать их, а также регистрироваться и логиниться. Я покажу код, объясню каждую часть, и покажу как я задеплоила проект на Render и подключила к фронтенду. - ---- - -### 2. Обзор package.json (~30 сек) - -Начнём с package.json. Здесь перечислены все библиотеки, которые я использую: - -- **express** — это фреймворк для создания сервера и обработки запросов -- **mongoose** — библиотека для работы с MongoDB (базой данных) -- **cors** — позволяет фронтенду обращаться к нашему API (без этого браузер заблокирует запросы) -- **bcrypt** — для шифрования паролей пользователей -- **dotenv** — для хранения секретных данных (например, ссылка на базу данных) в файле .env -- **nodemon** — автоматически перезапускает сервер при изменении кода (для разработки) -- **babel** — позволяет использовать современный JavaScript (import/export) - -Скрипт `npm run dev` запускает сервер в режиме разработки с nodemon. - ---- - -### 3. Импорты и настройка сервера (~40 сек) - -``` -import cors from "cors"; -import express from "express"; -import mongoose from "mongoose"; -import bcrypt from "bcrypt"; -import dotenv from "dotenv"; -``` - -В начале файла я импортирую все нужные библиотеки. - -Потом: - -``` -dotenv.config(); -``` -Это загружает переменные из файла `.env` — там хранится ссылка на базу данных. - -``` -const app = express(); -app.use(cors()); -app.use(express.json()); -``` - -- `express()` — создаёт наше приложение -- `cors()` — разрешает запросы с других доменов (например, с фронтенда) -- `express.json()` — говорит серверу понимать JSON в теле запросов - -``` -mongoose.connect(mongoURL); -``` -Эта строка подключает нас к MongoDB базе данных. - ---- - -### 4. Модели данных (~1 мин) - -**Модель User (Пользователь):** - -У каждого пользователя есть: -- `name` — имя (обязательное, минимум 2 символа) -- `email` — электронная почта (обязательное, уникальное, проверяется формат) -- `password` — пароль (обязательное, минимум 6 символов, хранится в зашифрованном виде) -- `accessToken` — токен для авторизации (генерируется автоматически) - -**Модель Thought (Мысль):** - -У каждой мысли есть: -- `message` — текст (от 5 до 140 символов) -- `hearts` — количество лайков (по умолчанию 0) -- `createdAt` — дата создания (автоматически) -- `user` — ссылка на пользователя, который создал мысль (это связь между коллекциями) - ---- - -### 5. Middleware для авторизации (~40 сек) - -``` -const auth = async (req, res, next) => { ... } -``` - -Это "прослойка" (middleware), которая проверяет — авторизован ли пользователь. - -Как она работает: -1. Берёт токен из заголовка запроса `Authorization` -2. Если токена нет — возвращает ошибку 401 "Not logged in" -3. Ищет пользователя с таким токеном в базе данных -4. Если не находит — возвращает ошибку 401 "Invalid token" -5. Если нашёл — сохраняет пользователя в `req.user` и пропускает дальше (вызывает `next()`) - -Я использую этот middleware на роутах, где нужна авторизация — создание, редактирование, удаление мыслей. - ---- - -### 6. Seed Database (~30 сек) - -``` -if (process.env.RESET_DB === "true") { ... } -``` - -Эта часть проверяет: если переменная `RESET_DB` равна `true`, то база данных очищается и заполняется 10 примерами мыслей. Это удобно для тестирования — всегда есть начальные данные. - ---- - -### 7. Роуты (endpoints) (~2 мин) - -**GET /** — корневой роут. Возвращает список всех доступных endpoint-ов. Это как документация API. - -**POST /register** — регистрация нового пользователя: -- Принимает имя, email и пароль -- Проверяет, не занят ли email -- Шифрует пароль с помощью bcrypt (пароль никогда не хранится в открытом виде!) -- Сохраняет пользователя и возвращает данные + токен - -**POST /login** — вход: -- Принимает email и пароль -- Ищет пользователя по email -- Сравнивает пароль с зашифрованным (bcrypt.compare) -- Если всё ок — возвращает данные + токен - -**GET /thoughts** — получить все мысли: -- Сортирует от новых к старым (`sort createdAt: -1`) -- Ограничивает до 20 штук (`limit(20)`) -- Подтягивает имя автора (`populate("user", "name")`) - -**GET /thoughts/:id** — получить одну мысль по её ID - -**POST /thoughts** (auth) — создать новую мысль: -- Требует авторизацию (middleware `auth`) -- Берёт текст из body и ID пользователя из `req.user` - -**PATCH /thoughts/:id** (auth) — обновить мысль: -- Требует авторизацию -- Проверяет, что ты автор этой мысли (нельзя редактировать чужие) - -**DELETE /thoughts/:id** (auth) — удалить мысль: -- Требует авторизацию -- Проверяет, что ты автор - -**POST /thoughts/:id/like** — лайкнуть мысль: -- Увеличивает `hearts` на 1 с помощью `$inc` -- Не требует авторизации — любой может лайкнуть - ---- - -### 8. Запуск сервера (~15 сек) - -``` -const port = process.env.PORT || 3000; -app.listen(port, () => { ... }); -``` - -Сервер запускается на порту из переменной окружения (Render сам задаёт PORT) или на порту 3000 при локальной разработке. - ---- - -### 9. Деплой на Render (~1 мин) - -Чтобы задеплоить проект: - -1. Я загрузила код на GitHub -2. Зашла на сайт render.com и создала новый Web Service -3. Подключила свой GitHub репозиторий -4. В настройках указала: - - **Root Directory**: `backend` (потому что мой server.js лежит в папке backend) - - **Build Command**: `npm install` - - **Start Command**: `npm start` -5. В разделе Environment Variables добавила: - - `MONGO_URL` — ссылку на MongoDB Atlas (облачную базу данных) - - `RESET_DB` — `true` (чтобы заполнить базу начальными данными) -6. Нажала Deploy — и через пару минут API стал доступен по ссылке от Render - -Теперь мой API работает онлайн и любой может к нему обращаться! - ---- - -### 10. Подключение к фронтенду Happy Thoughts (~1 мин) - -В моём фронтенд-проекте `js-project-happy-thoughts` я заменила URL API. - -Раньше фронтенд обращался к стандартному API от Technigo. Теперь он обращается к моему API на Render. - -Я просто заменила базовый URL в коде фронтенда, например: - -``` -const API_URL = "https://мой-проект.onrender.com"; -``` - -И теперь: -- Когда пользователь открывает страницу — фронтенд делает GET запрос на `/thoughts` и показывает все мысли -- Когда нажимает кнопку сердечка — отправляет POST на `/thoughts/:id/like` -- Когда пишет новую мысль — отправляет POST на `/thoughts` - -Фронтенд и бэкенд работают вместе, но хостятся отдельно — фронтенд на Netlify, бэкенд на Render. - ---- - -### 11. Тестирование в Postman (~1 мин) - -Postman — это инструмент для тестирования API без фронтенда. - -Примеры: - -1. **GET все мысли**: метод GET, URL: `https://мой-проект.onrender.com/thoughts` — получаем список мыслей - -2. **Регистрация**: метод POST, URL: `/register`, в Body (raw JSON): - ```json - { - "name": "Kausar", - "email": "kausar@test.com", - "password": "123456" - } - ``` - В ответе получаем accessToken — его нужно скопировать - -3. **Создать мысль** (с авторизацией): метод POST, URL: `/thoughts`, - - В Headers добавляем: `Authorization: <ваш_токен>` - - В Body: `{ "message": "Hello from Postman!" }` - -4. **Лайкнуть мысль**: метод POST, URL: `/thoughts//like` - -В Postman удобно тестировать все endpoint-ы, особенно те, которые требуют авторизацию — на фронтенде это сложнее проверить. - ---- - -### 12. MongoDB Compass и Atlas (~1 мин) - -**MongoDB Atlas** — это облачная база данных. Я создала бесплатный кластер (cluster) на mongodb.com. - -Из Atlas я получила строку подключения (connection string), которую использую как `MONGO_URL`. Она выглядит примерно так: -``` -mongodb+srv://username:password@cluster.mongodb.net/happythoughts -``` - -**MongoDB Compass** — это настольное приложение для просмотра базы данных. Я вставляю ту же строку подключения и могу: -- Видеть все коллекции (users, thoughts) -- Просматривать документы — каждая мысль, каждый пользователь -- Видеть зашифрованные пароли (они выглядят как длинная строка) -- Видеть связи между мыслями и пользователями через поле `user` (ObjectId) -- Редактировать или удалять данные вручную (для отладки) - ---- - -### 13. Заключение (~20 сек) - -Итого — я создала полноценный REST API с: -- Полным CRUD (создание, чтение, обновление, удаление) -- Авторизацией пользователей (регистрация, логин, токены) -- Шифрованием паролей -- Связями между моделями -- Деплоем на Render -- Подключением к фронтенду - -Спасибо за внимание! - ---- ---- - -## ENGLISH VERSION - ---- - -### 1. Introduction (~30 sec) - -Hi! Today I'll walk you through my backend project — Happy Thoughts API. It's a REST API built with Node.js, Express, and MongoDB. It allows users to create "thoughts", like them, and also register and log in. I'll go through the code, explain each part, and show how I deployed it to Render and connected it to my frontend. - ---- - -### 2. Package.json overview (~30 sec) - -Let's start with package.json. Here are all the libraries I'm using: - -- **express** — a framework for creating a server and handling requests -- **mongoose** — a library for working with MongoDB (our database) -- **cors** — allows the frontend to make requests to our API (without it, the browser would block requests) -- **bcrypt** — for hashing user passwords -- **dotenv** — for storing secret data (like the database URL) in a .env file -- **nodemon** — automatically restarts the server when code changes (for development) -- **babel** — allows us to use modern JavaScript syntax (import/export) - -The script `npm run dev` starts the server in development mode with nodemon. - ---- - -### 3. Imports and server setup (~40 sec) - -``` -import cors from "cors"; -import express from "express"; -import mongoose from "mongoose"; -import bcrypt from "bcrypt"; -import dotenv from "dotenv"; -``` - -At the top of the file, I import all the libraries I need. - -Then: - -``` -dotenv.config(); -``` -This loads variables from the `.env` file — that's where the database connection string is stored. - -``` -const app = express(); -app.use(cors()); -app.use(express.json()); -``` - -- `express()` — creates our application -- `cors()` — allows requests from other domains (like from our frontend) -- `express.json()` — tells the server to understand JSON in request bodies - -``` -mongoose.connect(mongoURL); -``` -This line connects us to the MongoDB database. - ---- - -### 4. Data Models (~1 min) - -**User Model:** - -Each user has: -- `name` — required, at least 2 characters -- `email` — required, unique, validated with a regex pattern -- `password` — required, at least 6 characters, stored encrypted -- `accessToken` — generated automatically, used for authentication - -**Thought Model:** - -Each thought has: -- `message` — the text (5 to 140 characters) -- `hearts` — number of likes (default 0) -- `createdAt` — creation date (set automatically) -- `user` — a reference to the user who created it (this links the two collections) - ---- - -### 5. Authentication Middleware (~40 sec) - -``` -const auth = async (req, res, next) => { ... } -``` - -This is a middleware — a function that runs before the route handler. - -How it works: -1. It takes the token from the `Authorization` header -2. If there's no token — returns 401 error "Not logged in" -3. Looks for a user with that token in the database -4. If not found — returns 401 "Invalid token" -5. If found — saves the user to `req.user` and calls `next()` to continue - -I use this middleware on routes that require authentication — creating, editing, and deleting thoughts. - ---- - -### 6. Seed Database (~30 sec) - -``` -if (process.env.RESET_DB === "true") { ... } -``` - -This part checks: if the `RESET_DB` environment variable is `true`, it clears the database and fills it with 10 sample thoughts. This is useful for testing — you always have some initial data. - ---- - -### 7. Routes (endpoints) (~2 min) - -**GET /** — the root route. Returns a list of all available endpoints. It works like API documentation. - -**POST /register** — register a new user: -- Accepts name, email, and password -- Checks if the email is already taken -- Hashes the password with bcrypt (passwords are never stored in plain text!) -- Saves the user and returns their data + token - -**POST /login** — log in: -- Accepts email and password -- Finds the user by email -- Compares the password with the hashed one (bcrypt.compare) -- If everything matches — returns user data + token - -**GET /thoughts** — get all thoughts: -- Sorted from newest to oldest (`sort createdAt: -1`) -- Limited to 20 (`limit(20)`) -- Includes the author's name (`populate("user", "name")`) - -**GET /thoughts/:id** — get a single thought by its ID - -**POST /thoughts** (auth) — create a new thought: -- Requires authentication (the `auth` middleware) -- Takes the message from the body and the user ID from `req.user` - -**PATCH /thoughts/:id** (auth) — update a thought: -- Requires authentication -- Checks that you are the author (you can't edit others' thoughts) - -**DELETE /thoughts/:id** (auth) — delete a thought: -- Requires authentication -- Checks that you are the author - -**POST /thoughts/:id/like** — like a thought: -- Increases `hearts` by 1 using `$inc` -- No authentication required — anyone can like - ---- - -### 8. Starting the server (~15 sec) - -``` -const port = process.env.PORT || 3000; -app.listen(port, () => { ... }); -``` - -The server starts on the port from the environment variable (Render sets PORT automatically) or on port 3000 for local development. - ---- - -### 9. Deploying to Render (~1 min) - -To deploy the project: - -1. I pushed the code to GitHub -2. Went to render.com and created a new Web Service -3. Connected my GitHub repository -4. In the settings I specified: - - **Root Directory**: `backend` (because my server.js is inside the backend folder) - - **Build Command**: `npm install` - - **Start Command**: `npm start` -5. In Environment Variables I added: - - `MONGO_URL` — the connection string from MongoDB Atlas (cloud database) - - `RESET_DB` — `true` (to seed the database with initial data) -6. Clicked Deploy — and in a couple of minutes the API was live with a Render URL - -Now my API is online and anyone can access it! - ---- - -### 10. Connecting to Happy Thoughts frontend (~1 min) - -In my frontend project `js-project-happy-thoughts`, I replaced the API URL. - -Before, the frontend was using the default Technigo API. Now it points to my own API on Render. - -I simply changed the base URL in the frontend code, for example: - -``` -const API_URL = "https://my-project.onrender.com"; -``` - -And now: -- When a user opens the page — the frontend makes a GET request to `/thoughts` and displays all thoughts -- When they click the heart button — it sends a POST to `/thoughts/:id/like` -- When they write a new thought — it sends a POST to `/thoughts` - -The frontend and backend work together but are hosted separately — frontend on Netlify, backend on Render. - ---- - -### 11. Testing with Postman (~1 min) - -Postman is a tool for testing APIs without a frontend. - -Examples: - -1. **GET all thoughts**: method GET, URL: `https://my-project.onrender.com/thoughts` — we get a list of thoughts - -2. **Register**: method POST, URL: `/register`, in Body (raw JSON): - ```json - { - "name": "Kausar", - "email": "kausar@test.com", - "password": "123456" - } - ``` - In the response we get an accessToken — copy it - -3. **Create a thought** (with auth): method POST, URL: `/thoughts`, - - In Headers add: `Authorization: ` - - In Body: `{ "message": "Hello from Postman!" }` - -4. **Like a thought**: method POST, URL: `/thoughts//like` - -Postman is great for testing all endpoints, especially the ones that require authentication — it's harder to test those from the frontend. - ---- - -### 12. MongoDB Compass and Atlas (~1 min) - -**MongoDB Atlas** — is a cloud database. I created a free cluster on mongodb.com. - -From Atlas I got a connection string, which I use as `MONGO_URL`. It looks something like: -``` -mongodb+srv://username:password@cluster.mongodb.net/happythoughts -``` - -**MongoDB Compass** — is a desktop app for viewing the database. I paste the same connection string and I can: -- See all collections (users, thoughts) -- Browse documents — each thought, each user -- See the encrypted passwords (they look like a long string) -- See the relationships between thoughts and users through the `user` field (ObjectId) -- Edit or delete data manually (for debugging) - ---- - -### 13. Conclusion (~20 sec) - -To sum up — I built a full REST API with: -- Full CRUD (Create, Read, Update, Delete) -- User authentication (register, login, tokens) -- Password hashing -- Model relationships -- Deployment on Render -- Connection to a frontend - -Thanks for watching!