diff --git a/README.md b/README.md index 6a75d8e1..857026e8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,20 @@ # Project Happy Thoughts API -Replace this readme with your own information about your project. +This project centers around creating a fully functional Happy Thoughts Messaging API using Node.js, Express, and MongoDB. The API integrates seamlessly with the Happy Thoughts React application, enabling users to fetch existing thoughts and post new ones through clearly defined endpoints. -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +## `Highlights` +- `RESTful Endpoints`: +Developed GET endpoints to retrieve a collection of recently created happy thoughts—limited to 20 results—and POST endpoints to add new thoughts or increment the "heart" count of existing ones. +- `Data Validation & Error Handling`: +Implemented robust input validation to ensure that all submissions meet the defined criteria. Returned meaningful error messages and set the appropriate HTTP status codes (notably 400 Bad Request) for invalid inputs, and 404 Not Found when modifying non-existent thoughts. +- `Clean Code & Scalability`: +Followed industry best practices for clarity, maintainability, and scalability. Utilized MongoDB and Mongoose to store and retrieve data, ensuring a stable and flexible database structure. -## The problem - -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +## `Technologies & Tools` +- `Node.js & Express`: Formed the backbone of the backend logic, efficiently handling routing and server management. +- `MongoDB & Mongoose`: Provided a document-based database solution, simplifying the process of storing, querying, and updating message entries. +- `Testing & Validation`: Leveraged Postman (or similar tools) to test and verify endpoints, ensuring the API behaved as intended and responded correctly under various scenarios. ## 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. +https://project-happy-thoughts-api-h0r6.onrender.com/ diff --git a/app.js b/app.js new file mode 100644 index 00000000..9f4fcc99 --- /dev/null +++ b/app.js @@ -0,0 +1,27 @@ +import express from "express"; +import cors from "cors"; +import bodyParser from "body-parser"; +import expressListEndpoints from "express-list-endpoints"; + +import { errorHandler } from "./middleware/errorHandler.js"; +import { thoughtRoutes } from "./routers/thoughtRoutes.js"; +import { likeRoutes } from "./routers/likeRoutes.js"; + +export const app = express(); + +// Add middlewares to enable cors and json body parsing +app.use(cors()); +app.use(bodyParser.json()); + +// Routes +app.use("/thoughts", thoughtRoutes); +app.use("/thoughts", likeRoutes); + +// API documentation +app.get("/", (req, res) => { + const endpoints = expressListEndpoints(app); + res.send(endpoints); +}); + +// Error handling middleware +app.use(errorHandler); \ No newline at end of file diff --git a/config/mongoDB.js b/config/mongoDB.js new file mode 100644 index 00000000..03e40c27 --- /dev/null +++ b/config/mongoDB.js @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; + +export const connectToMongoDB = async() => { + const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/mongoAPI"; + + try { + await mongoose.connect(mongoUrl); + mongoose.Promise = Promise; + console.log("Connected to MongoDB"); + } catch (error) { + console.error("Connection error", error.message); + process.exit(1); + } +} \ No newline at end of file diff --git a/controllers/likeController.js b/controllers/likeController.js new file mode 100644 index 00000000..45d9b290 --- /dev/null +++ b/controllers/likeController.js @@ -0,0 +1,22 @@ +import { Thought } from "../models/thoughModel.js"; + +// post a like +export const postLike = async (req, res) => { + try { + const { id } = req.params; + const updatedThought = await Thought.findByIdAndUpdate( + id, + { $inc: { hearts: 1 } }, + { new: true } + ); + + if (!updatedThought) { + return res.status(404).json({ error: "Thought not found" }); + } + + // Return the updated thought directly + res.status(200).json(updatedThought); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; \ No newline at end of file diff --git a/controllers/thoughtController.js b/controllers/thoughtController.js new file mode 100644 index 00000000..2f174993 --- /dev/null +++ b/controllers/thoughtController.js @@ -0,0 +1,27 @@ +import { Thought } from "../models/thoughModel.js"; + + +// Get all thoughts +export const getThoughts = async (req, res) => { + try { + const thoughts = await Thought.find().sort({ createdAt: "desc" }).limit(20).exec(); + // Just return the array of thoughts directly + res.status(200).json(thoughts); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// Post a new thought +export const postThought = async (req, res) => { + try { + const { message } = req.body; + const newThought = await new Thought({ message }).save(); + + // Return the newly created thought object directly + res.status(201).json(newThought); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}; + diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js new file mode 100644 index 00000000..f3831026 --- /dev/null +++ b/middleware/errorHandler.js @@ -0,0 +1,8 @@ +export const errorHandler = (err, req, res, next) => { + console.error(err.stack); +res.status(500 || err.status).json({ + success: false, + error: "Internal server error", + details: err.message + }); +} \ No newline at end of file diff --git a/middleware/validation.js b/middleware/validation.js new file mode 100644 index 00000000..2294b9da --- /dev/null +++ b/middleware/validation.js @@ -0,0 +1,20 @@ +import { body, validationResult } from "express-validator"; + +// Validation rules for creating a thought +export const validateThought = [ + body("message") + .trim() + .notEmpty() + .withMessage("Message is required.") + .isLength({ min: 5, max: 140 }) + .withMessage("Message must be between 5 and 140 characters."), +] + +// General validation handler +export const validate = (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + next(); +}; \ No newline at end of file diff --git a/models/thoughModel.js b/models/thoughModel.js new file mode 100644 index 00000000..89d1c358 --- /dev/null +++ b/models/thoughModel.js @@ -0,0 +1,20 @@ +import mongoose from "mongoose"; + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minlength: [5, "The message must be at least 5 characters long."], + maxlength: [140, "The message can't exceed 140 characters."] + }, + hearts: { + type: Number, + default: 0 + }, + createdAt: { + type: Date, + default: () => new Date(), + } +}); + +export const Thought = mongoose.model("Thought", thoughtSchema); \ No newline at end of file diff --git a/package.json b/package.json index 1c371b45..1bf4ef6b 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,29 @@ "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" }, - "author": "", + "author": "xing yin", "license": "ISC", "dependencies": { - "@babel/core": "^7.17.9", - "@babel/node": "^7.16.8", - "@babel/preset-env": "^7.16.11", + "@babel/core": "^7.26.0", + "@babel/node": "^7.26.0", + "@babel/preset-env": "^7.26.0", "cors": "^2.8.5", - "express": "^4.17.3", - "mongoose": "^8.0.0", - "nodemon": "^3.0.1" - } -} \ No newline at end of file + "dotenv": "^16.4.7", + "express": "^4.21.2", + "express-list-endpoints": "^7.1.1", + "express-validator": "^7.2.0", + "mongodb": "^6.12.0", + "mongoose": "^8.9.1", + "nodemon": "^3.1.9" + }, + "main": "server.js", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/xingyin2024/project-happy-thoughts-api.git" + }, + "bugs": { + "url": "https://github.com/xingyin2024/project-happy-thoughts-api/issues" + }, + "homepage": "https://github.com/xingyin2024/project-happy-thoughts-api#readme" +} diff --git a/routers/likeRoutes.js b/routers/likeRoutes.js new file mode 100644 index 00000000..0016d347 --- /dev/null +++ b/routers/likeRoutes.js @@ -0,0 +1,9 @@ +import express from "express"; +import { postLike } from "../controllers/likeController.js"; + +const router = express.Router(); + +// Like a thought +router.post("/:id/like", postLike); + +export { router as likeRoutes }; \ No newline at end of file diff --git a/routers/thoughtRoutes.js b/routers/thoughtRoutes.js new file mode 100644 index 00000000..206ee649 --- /dev/null +++ b/routers/thoughtRoutes.js @@ -0,0 +1,13 @@ +import express from "express"; +import { getThoughts, postThought } from "../controllers/thoughtController.js"; +import { validateThought, validate } from "../middleware/validation.js"; + +const router = express.Router(); + +// Get all thoughts +router.get("/", getThoughts); + +// Post a new thought +router.post("/", validateThought, validate, postThought); + +export { router as thoughtRoutes }; \ No newline at end of file diff --git a/server.js b/server.js index dfe86fb8..0fc29033 100644 --- a/server.js +++ b/server.js @@ -1,27 +1,25 @@ -import cors from "cors"; -import express from "express"; -import mongoose from "mongoose"; +import { connectToMongoDB } from "./config/mongoDB.js"; +import { app } from "./app.js" +import dotenv from "dotenv"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; -mongoose.connect(mongoUrl); -mongoose.Promise = Promise; -// 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 +dotenv.config(); + const port = process.env.PORT || 8080; -const app = express(); -// Add middlewares to enable cors and json body parsing -app.use(cors()); -app.use(express.json()); +(async () => { + try { + // Connet to mongoDB + await connectToMongoDB(); -// Start defining your routes here -app.get("/", (req, res) => { - res.send("Hello Technigo!"); -}); + // Start the server + app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); + }); + } catch (error) { + console.log("Failed to start server:", error.message); + process.exit(1); // Exit the process on failure + } +})(); -// Start the server -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); -}); +// Simplify server.js to handle only the server initialization and database connection \ No newline at end of file