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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"postman.settings.dotenv-detection-notification-visibility": false
}
61 changes: 55 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
# 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.
Backend API for sharing happy thoughts. Users can post thoughts, like others' thoughts, and manage their own content.

## Getting started
## Live Demo

Install dependencies with `npm install`, then start the server by running `npm run dev`
**API:** [https://js-project-api-cathi.onrender.com](https://js-project-api-cathi.onrender.com)

## View it live
## Tech Stack

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.
- Node.js & Express
- MongoDB with Mongoose
- JWT authentication
- bcrypt for passwords

## API Endpoints

### Authentication
- `POST /auth/register` - Register user
- `POST /auth/login` - Login user

### Thoughts
- `GET /thoughts` - Get all thoughts
- `POST /thoughts` - Create thought
- `PATCH /thoughts/:id` - Update thought (own only)
- `DELETE /thoughts/:id` - Delete thought (own only)
- `PATCH /thoughts/:id/like` - Like thought
- `PATCH /thoughts/:id/unlike` - Unlike thought

## Installation

1. Clone repo and install dependencies:
```bash
npm install
```

2. Create `.env` file:
```
MONGO_URL=mongodb://localhost/happy-thoughts
JWT_SECRET=your-secret-key
PORT=8080
```

3. Start development server:
```bash
npm run dev
```

## Features

- ✅ Anonymous and authenticated posting
- ✅ User authentication with JWT
- ✅ CRUD operations for thoughts
- ✅ Like/unlike functionality
- ✅ Input validation and error handling
- ✅ Pagination and filtering

## Deployment

Deployed on Render with MongoDB Atlas database.
73 changes: 73 additions & 0 deletions controllers/authController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { validationResult } from "express-validator";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { User } from "../models/User.js";

const JWT_SECRET = process.env.JWT_SECRET || "supersecret";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded fallback secret can be removed for security reasons.


// User registration
export const register = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

const { email, password } = req.body;
try {
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res
.status(409)
.json({ message: "That email address already exists" });
}

// Hash password
const hashedPassword = await bcrypt.hash(password, 10);

// Create and save user
const user = await User.create({ email, password: hashedPassword });

// Create JWT token
const token = jwt.sign(
{ userId: user._id, email: user.email },
JWT_SECRET,
{ expiresIn: "2h" }
);

res.status(201).json({ token, user: { id: user._id, email: user.email } });
} catch (error) {
res.status(500).json({ error: error.message });
}
};

// User login
export const login = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

const { email, password } = req.body;
try {
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ message: "Invalid credentials" });
}

const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.status(401).json({ message: "Invalid credentials" });
}

const token = jwt.sign(
{ userId: user._id, email: user.email },
JWT_SECRET,
{ expiresIn: "2h" }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem to have 2 hour duration but next step would be refreshed token.

);

res.status(200).json({ token, user: { id: user._id, email: user.email } });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
138 changes: 138 additions & 0 deletions controllers/thoughtsController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Thought } from "../models/Thought.js";
import { validationResult } from "express-validator";

// GET /thoughts
export const getThoughts = async (req, res) => {
const { message, minHearts, sort, page = 1, limit = 10 } = req.query;
try {
const query = {};
if (message) query.message = { $regex: new RegExp(message, "i") };
if (minHearts) query.hearts = { $gte: Number(minHearts) };

// inkludera user‐fältet så frontend ser ägaren
let thoughtsQuery = Thought.find(query).select(
"message hearts createdAt user"
);
if (sort === "most-liked")
thoughtsQuery = thoughtsQuery.sort({ hearts: -1 });
else if (sort === "least-liked")
thoughtsQuery = thoughtsQuery.sort({ hearts: 1 });

const total = await Thought.countDocuments(query);
const results = await thoughtsQuery

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do coercion on both number and page instead of just number.

"const { message, minHearts, sort, page = 1, limit = 10 } = req.query;

const pageNum = Number(page);
const limitNum = Number(limit);

.skip((pageNum - 1) * limitNum)
.limit(limitNum);"

.skip((page - 1) * limit)
.limit(Number(limit));

res.json({ page: Number(page), limit: Number(limit), total, results });
} catch (error) {
res.status(500).json({ error: "Server error", details: error });
}
};

// GET /thoughts/:id
export const getThoughtById = async (req, res) => {
try {
const thought = await Thought.findById(req.params.id).select(
"message hearts createdAt user"
);
if (!thought) return res.status(404).json({ error: "Thought not found" });
res.json(thought);
} catch (error) {
res.status(400).json({ error: "Invalid ID", details: error });
}
};

// POST /thoughts
export const createThought = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty())
return res.status(400).json({ errors: errors.array() });

try {
const { message } = req.body;
const newThought = new Thought({
message,
// If user is logged in (req.user exists), save their ID, otherwise null for anonymous
user: req.user?.id || null,
});
await newThought.save();
res.status(201).json(newThought);
} catch (error) {
res.status(400).json({ error: "Could not create thought", details: error });
}
};

// PATCH /thoughts/:id
export const updateThought = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty())
return res.status(400).json({ errors: errors.array() });

try {
const { id } = req.params;
const { message } = req.body;
const thought = await Thought.findById(id);
if (!thought) return res.status(404).json({ error: "Thought not found" });

// ägarskapskontroll
if (!thought.user.equals(req.user.id)) {
return res
.status(403)
.json({ error: "You can only update your own thoughts" });
}

thought.message = message;
await thought.save();
res.json(thought);
} catch (error) {
res.status(400).json({ error: "Invalid update", details: error });
}
};

// DELETE /thoughts/:id

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user can not delete thoughts if they're not logged in - potential new feature? Connecting it to the session ID or similar?

export const deleteThought = async (req, res) => {
try {
const thought = await Thought.findById(req.params.id);
if (!thought) return res.status(404).json({ error: "Thought not found" });

// ägarskapskontroll
if (!thought.user.equals(req.user.id)) {
return res
.status(403)
.json({ error: "You can only delete your own thoughts" });
}

await thought.deleteOne();
res.json({ message: "Thought deleted successfully" });
} catch (error) {
res.status(400).json({ error: "Invalid ID", details: error });
}
};

// PATCH /thoughts/:id/like
export const likeThought = async (req, res) => {
try {
const updated = await Thought.findByIdAndUpdate(
req.params.id,
{ $inc: { hearts: 1 } },
{ new: true }
);
if (!updated) return res.status(404).json({ error: "Thought not found" });
res.json(updated);
} catch (error) {
res.status(400).json({ error: "Failed to like thought", details: error });
}
};

// PATCH /thoughts/:id/unlike
export const unlikeThought = async (req, res) => {
Copy link

@oskarnordin oskarnordin Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good safety code that thoughts cant go negative. Keeping things positive or neutral at least.

Update: While logged in I can spam my own like-button and it does to negative -1 and -2 I managed to get.

try {
const thought = await Thought.findById(req.params.id);
if (!thought) return res.status(404).json({ error: "Thought not found" });
thought.hearts = Math.max(thought.hearts - 1, 0);
await thought.save();
res.json(thought);
} catch (error) {
res.status(400).json({ error: "Failed to unlike thought", details: error });
}
};
Loading