Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e064949
add routes for all messages, for a specific date and a specific ID
Jan 19, 2026
79bb3c7
add documentation of API to / route
Jan 19, 2026
afb32df
add error handling to message id route
Jan 19, 2026
cdc7562
add functionality for pagination in the route for all messages
Jan 20, 2026
3bfd934
add sorting functionality for date and likes with query parameters
Jan 20, 2026
efebf1b
add functionality for filtering
Jan 20, 2026
0a9313f
default to descending order in the sorting
Jan 20, 2026
46d9635
set up mongoDB with error handling + some first routes + temporary da…
Jan 23, 2026
f2f1574
add delete route for deleting a thought
Jan 23, 2026
ac9537b
add a patch route to be able to update the message of a thought + ref…
Jan 23, 2026
65a3e43
update delete route
Jan 23, 2026
9069a0b
add the functionality for filtering via mongoose
Jan 23, 2026
f798f15
add functionality for dynamic sorting via mongoose
Jan 23, 2026
bab940d
config dotenv + remove calling of seedDatabase + clean code
Jan 26, 2026
445b9ab
add patch route for updating the like count of a thought
Jan 26, 2026
aa1bb14
add editToken in schema to allow edit rights only for the message cre…
Jan 26, 2026
fc5cf29
update route name
Jan 27, 2026
aab1e65
change minlength of message in schema
Jan 27, 2026
6423923
add user model + create routes for sign-up and login + create functio…
Jan 29, 2026
c955e24
remove unique criteria from name in user schema
Jan 29, 2026
706771e
remove length limitations on password in user schema
Jan 30, 2026
9b9c367
remove all length limitations in user schema
Jan 30, 2026
3fdebaa
add user name in the server response in login and signup route
Jan 30, 2026
c5ab35c
add userId property in Thought schema for logged in users creating a …
Jan 30, 2026
7238ae6
add userId to each created message of the matching user to the access…
Jan 30, 2026
b6e0887
use middleware to attach user info to the request if there is an auth…
Feb 2, 2026
eb91d5e
remove code duplicates that should only be in middleware and not post…
Feb 2, 2026
fbc419b
add temporary logs
Feb 2, 2026
d44b870
add more temporary logs
Feb 2, 2026
5bdd556
fix bug (caused by removal of userId to early in GET route) so that i…
Feb 2, 2026
b60925d
change hearts in thought schema to an array of id's and update the pa…
Feb 2, 2026
8339ebc
update authenticateUser for better error handling
Feb 2, 2026
990688a
fix bug with filtering on hearts due to change in schema
Feb 2, 2026
b2f0f30
try another syntax for previous bug
Feb 2, 2026
891c8b1
change filterCritera for minimum likes
Feb 3, 2026
4dfbb95
add readme
Feb 3, 2026
a30870d
clean code
Feb 3, 2026
40d9670
fix big with filter and sorting combined after changing hearts in tho…
Feb 3, 2026
f54a00f
put routes and auth middleware in separate files for better organization
Feb 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
# 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.
This repository contains the backend API for Happy Thoughts, built with Node.js, Express, and MongoDB. The API handles authentication, authorization, data validation, and all CRUD operations for thoughts and users.

## Getting started
The API is fully RESTful and deployed to Render.

Install dependencies with `npm install`, then start the server by running `npm run dev`
## Live Site: https://happysharing.netlify.app/

## 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.
## Features

- User authentication (sign up & login)
- Password hashing with bcrypt
- Token-based authorization
- Create, read, update & delete thoughts
- Allow anonymous posting
- Like thoughts (authenticated & anonymous)
- Track which users liked which thoughts
- Fetch thoughts liked by the logged-in user
- Filtering & sorting thoughts: By date and number of likes
- Input validation & error handling
- Secure routes for authenticated actions only

---

## Tech Stack

- Node.js
- Express
- MongoDB
- Mongoose
- bcrypt
- RESTful API design
- Render (deployment)
36 changes: 36 additions & 0 deletions middlewares/authMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import User from "../models/User";


// Global middleware for authentication - To attach req.user everywhere
// I.e. if there is an accessToken in request header, find the matching user of it and attach it to every request
export const optionalAuth = async (req, res, next) => {
try {
const accessToken = req.headers.authorization;

if (!accessToken) {
return next();
}

const matchingUser = await User.findOne({ accessToken: accessToken });

if (matchingUser) {
req.user = matchingUser
}

next();

} catch(error) {
console.error("Optional auth error:", error)
next();
}
};


// To be used in routes that should only be accessed by authorized users
export const authenticateUser = (req, res, next) => {

if (!req.user) {
return res.status(401).json({ loggedOut: true });
}
next();
};
30 changes: 30 additions & 0 deletions models/Thought.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import mongoose from "mongoose";

const ThoughtSchema = new mongoose.Schema({
message: {
type: String,
required: true,
minlength: 1,
maxlength: 140
},
hearts: [
{
userId: { type: mongoose.Schema.Types.ObjectId, default: null }
}
],
createdAt: {
type: Date,
default: Date.now
},
editToken: {
type: String,
default: () => crypto.randomUUID()
},
// For logged-in users:
userId: {
type: mongoose.Schema.Types.ObjectId,
default: null
}
});

export default mongoose.model("Thought", ThoughtSchema);
24 changes: 24 additions & 0 deletions models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import mongoose from "mongoose";
import crypto from "crypto";

const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
},
accessToken : {
type: String,
default: () => crypto.randomBytes(128).toString("hex")
}
});

export default mongoose.model("User", UserSchema);
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcrypt-nodejs": "^0.0.3",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^4.17.3",
"express-list-endpoints": "^7.1.1",
"mongodb": "^7.0.0",
"mongoose": "^9.1.5",
"nodemon": "^3.0.1"
}
}
198 changes: 198 additions & 0 deletions routes/thoughtRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import express from "express";
import mongoose from "mongoose";
import Thought from "../models/Thought";
import { authenticateUser } from "../middlewares/authMiddleware";
import dotenv from "dotenv";
dotenv.config();

// Endpoint is /thoughts
const router = express.Router();


// All thoughts
router.get("/", async (req, res) => {

try {
const { minLikes, sortBy, order } = req.query;
const sortingOrder = order === "asc" ? 1 : -1;

// Variable for telling MongoDB how to prepare the data
const filterAndSort = [];

// Compute the like count from the hearts array, to use in the filtering
filterAndSort.push({
$addFields: {
likesCount: { $size: { $ifNull: ["$hearts", []] } } // Handle empty/null hearts
}
});

/* --- Functionality for filtering --- */
if (minLikes) {
filterAndSort.push({
$match: { likesCount: { $gte: Number(minLikes) } } //gte = Greater than or equals to
});
}

/* --- Functionality for sorting --- */
const sortCriteria = {};
if (sortBy === "date") {
sortCriteria.createdAt = sortingOrder;
} else if (sortBy === "likes") {
sortCriteria.likesCount = sortingOrder;
sortCriteria.createdAt = -1; // Secondary sort by date
} else {
sortCriteria.createdAt = -1; // Default sorting
}

filterAndSort.push({ $sort: sortCriteria });

/// Remove editToken to prevent it being exposed to users
filterAndSort.push({
$project: { editToken: 0 }
});

/* --- Execute filter and sorting --- */
const thoughts = await Thought.aggregate(filterAndSort);

const result = thoughts.map((thought) => {
const isCreator = req.user && thought.userId?.equals(req.user._id);
delete thought.userId; // Remove userId (after isCreator is computed) to prevent it from being exposed on front-end
return {
...thought,
isCreator
};
});
res.json(result);
} catch (error) {
console.error("GET /thoughts error:", error);
res.status(500).json({ message: "Failed to fetch thoughts", error: error.message });
}
});


// Post a thought
router.post("/", async (req, res) => {
try {
const message = req.body.message;

// Use mongoose model to create a database entry
const newThought = new Thought({
message,
userId: req.user ? req.user._id : null
});

const savedThought = await newThought.save();

res.status(201).json(savedThought);
} catch(error) {
res.status(400).json({
message: "Failed to save thought to database",
error: error.message
});
}
});


// Delete a thought
router.delete("/id/:id", async (req, res) => {
const id = req.params.id;

// Error handling for invalid id input
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: `Invalid id: ${id}` });
}

try {
const deletedThought = await Thought.findByIdAndDelete(id);

// Error handling for no ID match
if(!deletedThought) {
return res.status(404).json({ error: `Thought with id ${id} not found` });
}

res.json(deletedThought);

} catch(error) {
res.status(500).json({error: error.message});
}
});


// Update the like count of a thought
router.patch("/id/:id/like", async (req, res) => {
try {
const { id } = req.params;

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: `Invalid id: ${id}` });
}

const updatedThought = await Thought.findByIdAndUpdate(
id,
{ $push: { hearts: { userId: req.user ? req.user._id : null } } }, //Ensures the updated heart count gets returned, and that schema validation also is performed
{ new: true, runValidators: true }
);

// Error handling for no ID match
if(!updatedThought) {
return res.status(404).json({ error: `Thought with id ${id} not found` });
}

res.json(updatedThought);

} catch(error) {
res.status(500).json({ error: error.message });
}
});


// Update the message of a thought
router.patch("/id/:id/message", async (req, res) => {
const { id } = req.params;
const { message } = req.body;

// Error handling for invalid id input
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: `Invalid id: ${id}` });
}

try {
const updatedThought = await Thought.findByIdAndUpdate(
id,
{ message },
{ new: true, runValidators: true} //Ensures the updated message gets returned, and that schema validation also is performed on the new message
);

// Error handling for no ID match
if(!updatedThought) {
return res.status(404).json({error: `Thought with id ${id} not found`});
}

res.json(updatedThought);

} catch(err) {
res.status(500).json({error: err.message});
}
});


/* --- Authenticated only routes ---*/


// Liked thoughts
router.get("/liked", authenticateUser, async (req, res) => {
try {
const likedThoughts = await Thought
.find({ "hearts.userId": req.user._id })
.sort({ createdAt: -1 });

res.json(likedThoughts);

} catch (error) {
console.error("GET /thoughts error:", error);
res.status(500).json({ message: "Failed to fetch liked thoughts", error: error.message });
}
});


export default router;
Loading