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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 60 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,65 @@
# 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 is a RESTful API built with Node.js, Express, and MongoDB for a “Happy Thoughts” application.
Users can sign up, log in, post happy thoughts, like them, and manage their own posts.

## Getting started
The API is deployed on Render and connected to MongoDB Atlas.

Install dependencies with `npm install`, then start the server by running `npm run dev`
🚀 Features

## View it live
User authentication (signup & login)
Secure password hashing
Create, read, update, and delete thoughts
Like thoughts
Ownership protection (users can only edit/delete their own posts)
API documentation endpoint
Deployed online

🛠️ Tech Stack
Node.js
Express.js
MongoDB + Mongoose
bcryptjs (password hashing)
dotenv (environment variables)
express-list-endpoints

CORS

🧠 Data Models
User Model

email (unique)
password (hashed)
accessToken

HappyThought Model

message (5–140 chars)
hearts (number)
createdAt
userId (reference to User)

🔒 Security

Passwords are hashed using bcrypt
Access tokens are stored securely
Protected routes require authentication
Users can only edit/delete their own posts

❗ Error Handling

The API returns meaningful status codes:
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
500 Server Error

This project was built to practice:

REST APIs
Authentication
MongoDB
Full-stack integration
Deployment

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.
31 changes: 31 additions & 0 deletions middleware/authMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { User } from "../models/User.js";

export const authenticateUser = async (req, res, next) => {
try {
const authHeader = req.header("Authorization");

if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({
message: "Authentication missing or invalid.",
loggedOut: true,
});
}

const token = authHeader.replace("Bearer ", "").trim();
const user = await User.findOne({ accessToken: token });

if (!user) {
return res.status(401).json({
message: "Authentication missing or invalid.",
loggedOut: true,
});
}
req.user = user;
return next();
} catch (err) {
return res.status(500).json({
message: "Internal server error",
error: err.message,
});
}
};
26 changes: 26 additions & 0 deletions models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import mongoose from "mongoose";
import crypto from "crypto";

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

export const User = mongoose.model("User", UserSchema);
28 changes: 28 additions & 0 deletions models/happyThought.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import mongoose from "mongoose";

const HappyThoughtsSchema = new mongoose.Schema({
message: {
type: String,
required: true,
minlength: 5,
maxlength: 140,
},
hearts: {
type: Number,
default: 0,
},
createdAt: {
type: Date,
default: Date.now,
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
});

export const HappyThoughts = mongoose.model(
"HappyThoughts",
HappyThoughtsSchema,
);
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcryptjs": "^3.0.3",
"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"
}
}
143 changes: 143 additions & 0 deletions routes/happyThoughtRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import express from "express";
import mongoose from "mongoose";
import { HappyThoughts } from "../models/happyThought.js";
import { authenticateUser } from "../middleware/authMiddleware.js";

const router = express.Router();

router.get("/", async (req, res) => {
try {
const query = {};

if (req.query.minHearts) {
const minHearts = Number(req.query.minHearts);
if (Number.isNaN(minHearts)) {
return res.status(400).json({ error: "minHearts must be a number" });
}
query.hearts = { $gte: minHearts };
}

const thoughts = await HappyThoughts.find(query).sort({ createdAt: -1 }); //greater than or equal to
return res.json(thoughts);
} catch (err) {
res.status(500).json({ error: err.message });
}
});

router.get("/:id", async (req, res) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: "Invalid id" }); // will check if the id is valid
}
try {
const thought = await HappyThoughts.findById(id);
if (!thought) {
return res.status(404).json({ error: "Thought not found" });
}
return res.json(thought);
} catch (err) {
return res.status(500).json({ error: err.message });
}
});

router.post("/", authenticateUser, async (req, res) => {
const { message } = req.body;
if (!message || message.length < 5 || message.length > 140) {
return res.status(400).json({
error: "Message is required and must be between 5 and 140 characters", // will validate the message length or if it's empty
});
}
try {
const newThought = await HappyThoughts.create({
message,
userId: req.user.id,
});
return res.status(201).json(newThought);
} catch (err) {
return res.status(500).json({ error: err.message });
}
});

router.put("/:id", authenticateUser, async (req, res) => {
const { id } = req.params;
const { message } = req.body;

if (!message || message.length < 5 || message.length > 140) {
return res.status(400).json({
error: "Message is required and must be between 5 and 140 characters",
});
}

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

try {
const thought = await HappyThoughts.findById(id);
if (!thought) {
return res.status(404).json({ error: "Thought not found" });
}
if (thought.userId.toString() !== req.user.id) {
return res
.status(403)
.json({ error: "Unauthorized to update this thought" });
}

thought.message = message;
await thought.save();

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

router.delete("/:id", authenticateUser, async (req, res) => {
const { id } = req.params;

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

try {
const thought = await HappyThoughts.findById(id);
if (!thought) {
return res.status(404).json({ error: "Thought not found" });
}
if (thought.userId.toString() !== req.user.id) {
return res
.status(403)
.json({ error: "Unauthorized to delete this thought" });
}
await HappyThoughts.findByIdAndDelete(id);
return res.json({ message: "Thought deleted successfully" });
} catch (err) {
return res.status(500).json({ error: err.message });
}
});

router.post("/:id/like", async (req, res) => {
const { id } = req.params;

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

try {
const thought = await HappyThoughts.findByIdAndUpdate(
id,
{ $inc: { hearts: 1 } },
{ new: true },
);

if (!thought) {
return res.status(404).json({ error: "Thought not found" });
}

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

export default router;
Loading