From 7e1a9279c832bda6fafa9b1453f0d65af61e5bbf Mon Sep 17 00:00:00 2001 From: Mohammad Qabalany Date: Wed, 4 Feb 2026 00:46:30 +0100 Subject: [PATCH 1/6] Express API with endpoints --- data/thoughts.json | 26 +++++++++++++++++++++++++ package.json | 1 + server.js | 47 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 data/thoughts.json diff --git a/data/thoughts.json b/data/thoughts.json new file mode 100644 index 0000000..deeb9cb --- /dev/null +++ b/data/thoughts.json @@ -0,0 +1,26 @@ +[ + { + "id": "1", + "message": "I love learning backend development!", + "hearts": 5, + "createdAt": "2026-02-01T10:30:00Z" + }, + { + "id": "2", + "message": "Coffee and code make me happy", + "hearts": 12, + "createdAt": "2026-02-02T11:00:00Z" + }, + { + "id": "3", + "message": "Finally understanding Express.js!", + "hearts": 8, + "createdAt": "2026-02-03T14:20:00Z" + }, + { + "id": "4", + "message": "Building APIs is actually fun", + "hearts": 3, + "createdAt": "2026-02-04T09:15:00Z" + } +] diff --git a/package.json b/package.json index bf25bb6..00addae 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index f47771b..429eb02 100644 --- a/server.js +++ b/server.js @@ -1,19 +1,54 @@ import cors from "cors" import express from "express" +import listEndpoints from "express-list-endpoints" -// 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 +// Import hardcoded data +import thoughtsData from "./data/thoughts.json" + +// Define the port - defaults to 8080 or uses environment variable const port = process.env.PORT || 8080 const app = express() -// Add middlewares to enable cors and json body parsing +// Middlewares to enable CORS and JSON body parsing app.use(cors()) app.use(express.json()) -// Start defining your routes here +// Root endpoint - API documentation (auto-generated) +app.get("/", (req, res) => { + res.json({ + message: "🌟 Welcome to Happy Thoughts API!", + routes: listEndpoints(app) + }) +}) + +/* OLD manual documentation app.get("/", (req, res) => { - res.send("Hello Technigo!") + res.json({ + message: "🌟 Welcome to Happy Thoughts API!", + endpoints: { + "/": "API documentation (you are here)", + "/thoughts": "GET all thoughts", + "/thoughts/:id": "GET a single thought by ID" + } + }) +}) +*/ + +// GET /thoughts - Return all thoughts +app.get("/thoughts", (req, res) => { + res.json(thoughtsData) +}) + +// GET /thoughts/:id - Return a single thought by ID +app.get("/thoughts/:id", (req, res) => { + const { id } = req.params + const thought = thoughtsData.find((t) => t.id === id) + + if (thought) { + res.json(thought) + } else { + res.status(404).json({ error: "Thought not found" }) + } }) // Start the server From 55a6d76a19d62bab781a5b3c7846cf9ebee2b8e4 Mon Sep 17 00:00:00 2001 From: Mohammad Qabalany Date: Wed, 4 Feb 2026 03:08:49 +0100 Subject: [PATCH 2/6] Add MongoDB integration with full CRUD operations --- models/Thought.js | 26 +++++++ package.json | 2 + server.js | 174 ++++++++++++++++++++++++++++++++++++++++++---- test.html | 42 +++++++++++ 4 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 models/Thought.js create mode 100644 test.html diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..9c3fab3 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,26 @@ +import mongoose from "mongoose" + +// Define the Thought schema +const ThoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: [true, "Message is required"], + minlength: [5, "Message must be at least 5 characters"], + maxlength: [140, "Message cannot exceed 140 characters"], + trim: true + }, + hearts: { + type: Number, + default: 0, + min: 0 + }, + createdAt: { + type: Date, + default: Date.now + } +}) + +// Create the Thought model +const Thought = mongoose.model("Thought", ThoughtSchema) + +export default Thought diff --git a/package.json b/package.json index 00addae..e3f8894 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", + "dotenv": "^17.2.3", "express": "^4.17.3", "express-list-endpoints": "^7.1.1", + "mongoose": "^9.1.5", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index 429eb02..749d2cf 100644 --- a/server.js +++ b/server.js @@ -1,14 +1,27 @@ import cors from "cors" import express from "express" import listEndpoints from "express-list-endpoints" +import mongoose from "mongoose" +import dotenv from "dotenv" -// Import hardcoded data -import thoughtsData from "./data/thoughts.json" +// Load environment variables +dotenv.config() + +// Import hardcoded data (no longer needed - using MongoDB now) +// import thoughtsData from "./data/thoughts.json" + +// Import Thought model +import Thought from "./models/Thought.js" // Define the port - defaults to 8080 or uses environment variable const port = process.env.PORT || 8080 const app = express() +// Connect to MongoDB +mongoose.connect(process.env.MONGODB_URI) + .then(() => console.log("✅ Connected to MongoDB")) + .catch((error) => console.error("❌ MongoDB connection error:", error)) + // Middlewares to enable CORS and JSON body parsing app.use(cors()) app.use(express.json()) @@ -34,20 +47,153 @@ app.get("/", (req, res) => { }) */ -// GET /thoughts - Return all thoughts -app.get("/thoughts", (req, res) => { - res.json(thoughtsData) +// GET /thoughts - Return all thoughts from database +app.get("/thoughts", async (req, res) => { + try { + const thoughts = await Thought.find() + .sort({ createdAt: -1 }) // Sort by newest first + .limit(20) // Limit to 20 thoughts + res.json(thoughts) + } catch (error) { + res.status(500).json({ + error: "Failed to fetch thoughts", + message: error.message + }) + } +}) + +// GET /thoughts/:id - Return a single thought by ID from database +app.get("/thoughts/:id", async (req, res) => { + try { + const { id } = req.params + const thought = await Thought.findById(id) + + if (thought) { + res.json(thought) + } else { + res.status(404).json({ error: "Thought not found" }) + } + } catch (error) { + res.status(400).json({ + error: "Invalid thought ID", + message: error.message + }) + } +}) + +// POST /thoughts - Create a new thought +app.post("/thoughts", async (req, res) => { + try { + const { message } = req.body + + // Create new thought + const thought = new Thought({ message }) + + // Save to database + const savedThought = await thought.save() + + // Return created thought with 201 status + res.status(201).json(savedThought) + } catch (error) { + // Handle validation errors + if (error.name === "ValidationError") { + const errors = Object.values(error.errors).map(err => err.message) + res.status(400).json({ + error: "Validation failed", + messages: errors + }) + } else { + res.status(500).json({ + error: "Failed to create thought", + message: error.message + }) + } + } +}) + +// POST /thoughts/:id/like - Increment hearts count +app.post("/thoughts/:id/like", async (req, res) => { + try { + const { id } = req.params + + // Find thought and increment hearts by 1 + const thought = await Thought.findByIdAndUpdate( + id, + { $inc: { hearts: 1 } }, // Increment hearts by 1 + { new: true } // Return updated document + ) + + if (thought) { + res.json(thought) + } else { + res.status(404).json({ error: "Thought not found" }) + } + } catch (error) { + res.status(400).json({ + error: "Failed to like thought", + message: error.message + }) + } }) -// GET /thoughts/:id - Return a single thought by ID -app.get("/thoughts/:id", (req, res) => { - const { id } = req.params - const thought = thoughtsData.find((t) => t.id === id) - - if (thought) { - res.json(thought) - } else { - res.status(404).json({ error: "Thought not found" }) +// PUT /thoughts/:id - Update a thought's message +app.put("/thoughts/:id", async (req, res) => { + try { + const { id } = req.params + const { message } = req.body + + // Find and update thought with validation + const thought = await Thought.findByIdAndUpdate( + id, + { message }, + { + new: true, // Return updated document + runValidators: true // Run schema validation + } + ) + + if (thought) { + res.json(thought) + } else { + res.status(404).json({ error: "Thought not found" }) + } + } catch (error) { + if (error.name === "ValidationError") { + const errors = Object.values(error.errors).map(err => err.message) + res.status(400).json({ + error: "Validation failed", + messages: errors + }) + } else { + res.status(400).json({ + error: "Failed to update thought", + message: error.message + }) + } + } +}) + +// DELETE /thoughts/:id - Delete a thought +app.delete("/thoughts/:id", async (req, res) => { + try { + const { id } = req.params + + const thought = await Thought.findByIdAndDelete(id) + + if (thought) { + res.json({ + success: true, + message: "Thought deleted successfully", + deletedThought: thought + }) + } else { + res.status(404).json({ error: "Thought not found" }) + } + } catch (error) { + res.status(400).json({ + error: "Failed to delete thought", + message: error.message + }) } }) diff --git a/test.html b/test.html new file mode 100644 index 0000000..0b4c33a --- /dev/null +++ b/test.html @@ -0,0 +1,42 @@ + + + + Test Happy Thoughts API + + + +

🌟 Happy Thoughts API Test

+ +

Add New Thought

+ + + +
+ + + + From 0443c7257f71f95719993c87a2de4eb3f1fec34b Mon Sep 17 00:00:00 2001 From: Mohammad Qabalany Date: Sun, 8 Feb 2026 00:13:30 +0100 Subject: [PATCH 3/6] Add auth: User model, signup/login, JWT middleware, protected routes --- models/Thought.js | 5 + models/User.js | 61 +++++++++++ package.json | 2 + server.js | 260 ++++++++++++++++++++++++++++++++++------------ test.html | 42 -------- 5 files changed, 261 insertions(+), 109 deletions(-) create mode 100644 models/User.js delete mode 100644 test.html diff --git a/models/Thought.js b/models/Thought.js index 9c3fab3..5cab679 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -14,6 +14,11 @@ const ThoughtSchema = new mongoose.Schema({ default: 0, min: 0 }, + author: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + default: null // null = anonymous thought + }, createdAt: { type: Date, default: Date.now diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..77ba6b4 --- /dev/null +++ b/models/User.js @@ -0,0 +1,61 @@ +import mongoose from "mongoose" +import bcrypt from "bcrypt" + +// Define the User schema +const UserSchema = new mongoose.Schema({ + username: { + type: String, + required: [true, "Username is required"], + unique: true, + minlength: [3, "Username must be at least 3 characters"], + maxlength: [30, "Username cannot exceed 30 characters"], + trim: true + }, + email: { + type: String, + required: [true, "Email is required"], + unique: true, + trim: true, + lowercase: true, + match: [/^\S+@\S+\.\S+$/, "Please enter a valid email address"] + }, + password: { + type: String, + required: [true, "Password is required"], + minlength: [6, "Password must be at least 6 characters"] + }, + createdAt: { + type: Date, + default: Date.now + } +}) + +// Hash password before saving +UserSchema.pre("save", async function (next) { + // Only hash if password is new or modified + if (!this.isModified("password")) return next() + + try { + const salt = await bcrypt.genSalt(10) + this.password = await bcrypt.hash(this.password, salt) + next() + } catch (error) { + next(error) + } +}) + +// Method to compare passwords +UserSchema.methods.comparePassword = async function (candidatePassword) { + return bcrypt.compare(candidatePassword, this.password) +} + +// Remove password from JSON output +UserSchema.methods.toJSON = function () { + const user = this.toObject() + delete user.password + return user +} + +const User = mongoose.model("User", UserSchema) + +export default User diff --git a/package.json b/package.json index e3f8894..a44853e 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.17.3", "express-list-endpoints": "^7.1.1", + "jsonwebtoken": "^9.0.3", "mongoose": "^9.1.5", "nodemon": "^3.0.1" } diff --git a/server.js b/server.js index 749d2cf..2e8e714 100644 --- a/server.js +++ b/server.js @@ -3,15 +3,14 @@ import express from "express" import listEndpoints from "express-list-endpoints" import mongoose from "mongoose" import dotenv from "dotenv" +import jwt from "jsonwebtoken" // Load environment variables dotenv.config() -// Import hardcoded data (no longer needed - using MongoDB now) -// import thoughtsData from "./data/thoughts.json" - -// Import Thought model +// Import models import Thought from "./models/Thought.js" +import User from "./models/User.js" // Define the port - defaults to 8080 or uses environment variable const port = process.env.PORT || 8080 @@ -26,7 +25,53 @@ mongoose.connect(process.env.MONGODB_URI) app.use(cors()) app.use(express.json()) -// Root endpoint - API documentation (auto-generated) +// ============================================= +// AUTH MIDDLEWARE +// ============================================= + +// Middleware to verify JWT token (required auth) +const authenticate = async (req, res, next) => { + try { + const authHeader = req.headers.authorization + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ error: "Access denied. No token provided." }) + } + + const token = authHeader.replace("Bearer ", "") + const decoded = jwt.verify(token, process.env.JWT_SECRET) + + const user = await User.findById(decoded.userId) + if (!user) { + return res.status(401).json({ error: "Invalid token. User not found." }) + } + + req.user = user + next() + } catch (error) { + res.status(401).json({ error: "Invalid or expired token." }) + } +} + +// Optional auth: attaches user if token present, but doesn't require it +const optionalAuth = async (req, res, next) => { + try { + const authHeader = req.headers.authorization + if (authHeader && authHeader.startsWith("Bearer ")) { + const token = authHeader.replace("Bearer ", "") + const decoded = jwt.verify(token, process.env.JWT_SECRET) + const user = await User.findById(decoded.userId) + if (user) req.user = user + } + } catch (error) { + // Token invalid - just continue without user + } + next() +} + +// ============================================= +// ROOT ENDPOINT +// ============================================= + app.get("/", (req, res) => { res.json({ message: "🌟 Welcome to Happy Thoughts API!", @@ -34,25 +79,103 @@ app.get("/", (req, res) => { }) }) -/* OLD manual documentation -app.get("/", (req, res) => { - res.json({ - message: "🌟 Welcome to Happy Thoughts API!", - endpoints: { - "/": "API documentation (you are here)", - "/thoughts": "GET all thoughts", - "/thoughts/:id": "GET a single thought by ID" +// ============================================= +// AUTH ENDPOINTS +// ============================================= + +// POST /signup - Register a new user +app.post("/signup", async (req, res) => { + try { + const { username, email, password } = req.body + + // Check if user already exists + const existingUser = await User.findOne({ + $or: [{ email }, { username }] + }) + if (existingUser) { + const field = existingUser.email === email ? "email" : "username" + return res.status(400).json({ + error: `An account with this ${field} already exists.`, + field + }) } - }) + + // Create and save new user + const user = new User({ username, email, password }) + const savedUser = await user.save() + + // Generate token + const token = jwt.sign( + { userId: savedUser._id }, + process.env.JWT_SECRET, + { expiresIn: "7d" } + ) + + res.status(201).json({ + user: savedUser, + token + }) + } catch (error) { + if (error.name === "ValidationError") { + const errors = {} + Object.keys(error.errors).forEach(key => { + errors[key] = error.errors[key].message + }) + res.status(400).json({ error: "Validation failed", errors }) + } else { + res.status(500).json({ error: "Failed to create account", message: error.message }) + } + } }) -*/ -// GET /thoughts - Return all thoughts from database +// POST /login - Login and get token +app.post("/login", async (req, res) => { + try { + const { email, password } = req.body + + if (!email || !password) { + return res.status(400).json({ error: "Email and password are required." }) + } + + // Find user by email + const user = await User.findOne({ email: email.toLowerCase() }) + if (!user) { + return res.status(401).json({ error: "Invalid email or password." }) + } + + // Compare password + const isMatch = await user.comparePassword(password) + if (!isMatch) { + return res.status(401).json({ error: "Invalid email or password." }) + } + + // Generate token + const token = jwt.sign( + { userId: user._id }, + process.env.JWT_SECRET, + { expiresIn: "7d" } + ) + + res.json({ + user, + token + }) + } catch (error) { + res.status(500).json({ error: "Login failed", message: error.message }) + } +}) + +// ============================================= +// THOUGHT ENDPOINTS +// ============================================= + +// GET /thoughts - Return all thoughts (public) app.get("/thoughts", async (req, res) => { try { const thoughts = await Thought.find() - .sort({ createdAt: -1 }) // Sort by newest first - .limit(20) // Limit to 20 thoughts + .sort({ createdAt: -1 }) + .limit(20) + .populate("author", "username") res.json(thoughts) } catch (error) { res.status(500).json({ @@ -62,11 +185,11 @@ app.get("/thoughts", async (req, res) => { } }) -// GET /thoughts/:id - Return a single thought by ID from database +// GET /thoughts/:id - Return a single thought (public) app.get("/thoughts/:id", async (req, res) => { try { const { id } = req.params - const thought = await Thought.findById(id) + const thought = await Thought.findById(id).populate("author", "username") if (thought) { res.json(thought) @@ -81,21 +204,25 @@ 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 (authenticated) +app.post("/thoughts", optionalAuth, async (req, res) => { try { const { message } = req.body - // Create new thought - const thought = new Thought({ message }) - - // Save to database + const thoughtData = { message } + // If user is logged in, link the thought to them + if (req.user) { + thoughtData.author = req.user._id + } + + const thought = new Thought(thoughtData) const savedThought = await thought.save() + + // Populate author before returning + const populated = await savedThought.populate("author", "username") - // Return created thought with 201 status - res.status(201).json(savedThought) + res.status(201).json(populated) } catch (error) { - // Handle validation errors if (error.name === "ValidationError") { const errors = Object.values(error.errors).map(err => err.message) res.status(400).json({ @@ -111,17 +238,16 @@ app.post("/thoughts", async (req, res) => { } }) -// POST /thoughts/:id/like - Increment hearts count +// POST /thoughts/:id/like - Increment hearts count (public) app.post("/thoughts/:id/like", async (req, res) => { try { const { id } = req.params - // Find thought and increment hearts by 1 const thought = await Thought.findByIdAndUpdate( id, - { $inc: { hearts: 1 } }, // Increment hearts by 1 - { new: true } // Return updated document - ) + { $inc: { hearts: 1 } }, + { new: true } + ).populate("author", "username") if (thought) { res.json(thought) @@ -136,59 +262,59 @@ app.post("/thoughts/:id/like", async (req, res) => { } }) -// PUT /thoughts/:id - Update a thought's message -app.put("/thoughts/:id", async (req, res) => { +// PUT /thoughts/:id - Update a thought (authenticated, owner only) +app.put("/thoughts/:id", authenticate, async (req, res) => { try { const { id } = req.params const { message } = req.body + + // Check ownership + const existing = await Thought.findById(id) + if (!existing) { + return res.status(404).json({ error: "Thought not found" }) + } + if (existing.author && existing.author.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only edit your own thoughts." }) + } - // Find and update thought with validation const thought = await Thought.findByIdAndUpdate( id, { message }, - { - new: true, // Return updated document - runValidators: true // Run schema validation - } - ) + { new: true, runValidators: true } + ).populate("author", "username") - if (thought) { - res.json(thought) - } else { - res.status(404).json({ error: "Thought not found" }) - } + res.json(thought) } catch (error) { if (error.name === "ValidationError") { const errors = Object.values(error.errors).map(err => err.message) - res.status(400).json({ - error: "Validation failed", - messages: errors - }) + res.status(400).json({ error: "Validation failed", messages: errors }) } else { - res.status(400).json({ - error: "Failed to update thought", - message: error.message - }) + res.status(400).json({ error: "Failed to update thought", message: error.message }) } } }) -// DELETE /thoughts/:id - Delete a thought -app.delete("/thoughts/:id", async (req, res) => { +// DELETE /thoughts/:id - Delete a thought (authenticated, owner only) +app.delete("/thoughts/:id", authenticate, async (req, res) => { try { const { id } = req.params - const thought = await Thought.findByIdAndDelete(id) - - if (thought) { - res.json({ - success: true, - message: "Thought deleted successfully", - deletedThought: thought - }) - } else { - res.status(404).json({ error: "Thought not found" }) + // Check ownership + const existing = await Thought.findById(id) + if (!existing) { + return res.status(404).json({ error: "Thought not found" }) + } + if (existing.author && existing.author.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only delete your own thoughts." }) } + + await Thought.findByIdAndDelete(id) + + res.json({ + success: true, + message: "Thought deleted successfully", + deletedThought: existing + }) } catch (error) { res.status(400).json({ error: "Failed to delete thought", diff --git a/test.html b/test.html deleted file mode 100644 index 0b4c33a..0000000 --- a/test.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - Test Happy Thoughts API - - - -

🌟 Happy Thoughts API Test

- -

Add New Thought

- - - -
- - - - From 0950d08b43ea50894e6ca9f6ac924573028ed6ec Mon Sep 17 00:00:00 2001 From: Mohammad Qabalany Date: Sun, 8 Feb 2026 00:43:40 +0100 Subject: [PATCH 4/6] Fix:async pre-save hook --- models/User.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/models/User.js b/models/User.js index 77ba6b4..8800263 100644 --- a/models/User.js +++ b/models/User.js @@ -31,17 +31,12 @@ const UserSchema = new mongoose.Schema({ }) // Hash password before saving -UserSchema.pre("save", async function (next) { +UserSchema.pre("save", async function () { // Only hash if password is new or modified - if (!this.isModified("password")) return next() + if (!this.isModified("password")) return - try { - const salt = await bcrypt.genSalt(10) - this.password = await bcrypt.hash(this.password, salt) - next() - } catch (error) { - next(error) - } + const salt = await bcrypt.genSalt(10) + this.password = await bcrypt.hash(this.password, salt) }) // Method to compare passwords From f21f6394df919c838c6952e95cf13d53dc704c13 Mon Sep 17 00:00:00 2001 From: Mohammad Qabalany Date: Sun, 8 Feb 2026 00:54:18 +0100 Subject: [PATCH 5/6] Add filtering, sorting & pagination to GET /thoughts --- server.js | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 2e8e714..7950009 100644 --- a/server.js +++ b/server.js @@ -169,14 +169,48 @@ app.post("/login", async (req, res) => { // THOUGHT ENDPOINTS // ============================================= -// GET /thoughts - Return all thoughts (public) +// GET /thoughts - Return thoughts with filtering, sorting & pagination app.get("/thoughts", async (req, res) => { try { - const thoughts = await Thought.find() - .sort({ createdAt: -1 }) - .limit(20) + const { sort, order, minHearts, page, limit: limitParam } = req.query + + // Build filter + const filter = {} + if (minHearts) { + filter.hearts = { $gte: Number(minHearts) } + } + + // Build sort option (default: newest first) + let sortOption = { createdAt: -1 } + if (sort === "hearts") { + sortOption = { hearts: order === "asc" ? 1 : -1 } + } else if (sort === "date") { + sortOption = { createdAt: order === "asc" ? 1 : -1 } + } + + // Pagination + const pageNum = Math.max(1, Number(page) || 1) + const perPage = Math.min(100, Math.max(1, Number(limitParam) || 20)) + const skip = (pageNum - 1) * perPage + + // Count total for pagination info + const total = await Thought.countDocuments(filter) + + const thoughts = await Thought.find(filter) + .sort(sortOption) + .skip(skip) + .limit(perPage) .populate("author", "username") - res.json(thoughts) + + res.json({ + thoughts, + pagination: { + page: pageNum, + limit: perPage, + total, + totalPages: Math.ceil(total / perPage) + } + }) } catch (error) { res.status(500).json({ error: "Failed to fetch thoughts", From 30361d720cac68d0bba3693c4938fb1dc2906d05 Mon Sep 17 00:00:00 2001 From: Mohammad Qabalany Date: Sun, 8 Feb 2026 17:10:44 +0100 Subject: [PATCH 6/6] Update README --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0f9f073..6a80a66 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,53 @@ -# 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. +A REST API for the Happy Thoughts app. Built with Express.js and MongoDB. -## Getting started +**Live:** https://js-project-api-7ve2.onrender.com -Install dependencies with `npm install`, then start the server by running `npm run dev` +**Frontend:** [js-project-happy-thoughts](https://github.com/Qabalany/js-project-happy-thoughts) -## View it live +## Tech -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. +Express, MongoDB Atlas, Mongoose, bcrypt, JWT, dotenv, Babel, Render + +## Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/` | API docs | +| GET | `/thoughts` | Get all thoughts | +| GET | `/thoughts/:id` | Get one thought | +| POST | `/thoughts` | Create thought | +| POST | `/thoughts/:id/like` | Like a thought | +| PUT | `/thoughts/:id` | Update thought (auth required) | +| DELETE | `/thoughts/:id` | Delete thought (auth required) | +| POST | `/signup` | Register user | +| POST | `/login` | Login and get token | + +### Query Params (GET /thoughts) + +| Param | Default | Example | +|-------|---------|---------| +| `sort` | `date` | `?sort=hearts` | +| `order` | `desc` | `?order=asc` | +| `minHearts` | — | `?minHearts=5` | +| `page` | `1` | `?page=2` | +| `limit` | `20` | `?limit=10` | + +## Run Locally + +```bash +npm install +npm run dev +``` + +Create a `.env` file: +``` +MONGODB_URI=your_mongodb_connection_string +JWT_SECRET=your_secret_key +PORT=8080 +``` + +## View It Live + +https://js-project-api-7ve2.onrender.com