From c7dab1c8e9c63a99586a026818ccb5ce587eac2f Mon Sep 17 00:00:00 2001 From: Matthewtizon Date: Mon, 1 Jun 2026 15:52:49 +0800 Subject: [PATCH] Implement todo API --- backend/index.js | 13 --- backend/package.json | 18 +++- backend/src/app.js | 30 ++++++ backend/src/controllers/todoController.js | 118 ++++++++++++++++++++++ backend/src/middleware/errorHandler.js | 20 ++++ backend/src/routes/todoRoutes.js | 19 ++++ backend/src/server.js | 7 ++ backend/src/services/todoService.js | 61 +++++++++++ backend/src/utils/validators.js | 12 +++ 9 files changed, 281 insertions(+), 17 deletions(-) delete mode 100644 backend/index.js create mode 100644 backend/src/app.js create mode 100644 backend/src/controllers/todoController.js create mode 100644 backend/src/middleware/errorHandler.js create mode 100644 backend/src/routes/todoRoutes.js create mode 100644 backend/src/server.js create mode 100644 backend/src/services/todoService.js create mode 100644 backend/src/utils/validators.js diff --git a/backend/index.js b/backend/index.js deleted file mode 100644 index ff6a6a3b..00000000 --- a/backend/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const express = require("express"); -const app = express(); -const PORT = process.env.PORT || 4000; - -// Basic route -app.get("/", (req, res) => { - res.send("Hello from Express!"); -}); - -// Start server -app.listen(PORT, () => { - console.log(`Backend is running on http://localhost:${PORT}`); -}); diff --git a/backend/package.json b/backend/package.json index c85981fa..796cf9ba 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,10 +1,20 @@ { - "name": "backend", + "name": "codebility-backend-assessment", "version": "1.0.0", + "description": "Simple Todo API for Codebility backend assessment", + "main": "src/server.js", "scripts": { - "start": "node index.js" + "dev": "nodemon src/server.js", + "start": "node src/server.js" }, + "keywords": [], + "author": "John Matthew Tizon", + "license": "ISC", "dependencies": { - "express": "^4.18.2" + "cors": "^2.8.5", + "express": "^5.1.0" + }, + "devDependencies": { + "nodemon": "^3.1.10" } -} +} \ No newline at end of file diff --git a/backend/src/app.js b/backend/src/app.js new file mode 100644 index 00000000..d5c37ffa --- /dev/null +++ b/backend/src/app.js @@ -0,0 +1,30 @@ +const express = require("express"); +const cors = require("cors"); + +const todoRoutes = require("./routes/todoRoutes"); +const { notFoundHandler, errorHandler } = require("./middleware/errorHandler"); + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +app.get("/", (req, res) => { + res.status(200).json({ + message: "Todo API is running", + endpoints: { + listTodos: "GET /api/todos", + getTodo: "GET /api/todos/:id", + createTodo: "POST /api/todos", + updateTodo: "PUT /api/todos/:id", + deleteTodo: "DELETE /api/todos/:id" + } + }); +}); + +app.use("/api/todos", todoRoutes); + +app.use(notFoundHandler); +app.use(errorHandler); + +module.exports = app; \ No newline at end of file diff --git a/backend/src/controllers/todoController.js b/backend/src/controllers/todoController.js new file mode 100644 index 00000000..5146f8ff --- /dev/null +++ b/backend/src/controllers/todoController.js @@ -0,0 +1,118 @@ +const todoService = require("../services/todoService"); +const { isValidTitle, isValidCompleted } = require("../utils/validators"); + +function getAllTodos(req, res) { + const todos = todoService.getAllTodos(); + + res.status(200).json({ + success: true, + count: todos.length, + data: todos + }); +} + +function getTodoById(req, res) { + const todo = todoService.getTodoById(req.params.id); + + if (!todo) { + return res.status(404).json({ + success: false, + message: "Todo not found" + }); + } + + res.status(200).json({ + success: true, + data: todo + }); +} + +function createTodo(req, res) { + const { title, completed } = req.body; + + if (!isValidTitle(title)) { + return res.status(400).json({ + success: false, + message: "Title is required and must be a non-empty string" + }); + } + + if (completed !== undefined && !isValidCompleted(completed)) { + return res.status(400).json({ + success: false, + message: "Completed must be a boolean" + }); + } + + const todo = todoService.createTodo({ + title, + completed + }); + + res.status(201).json({ + success: true, + message: "Todo created successfully", + data: todo + }); +} + +function updateTodo(req, res) { + const { title, completed } = req.body; + + if (title !== undefined && !isValidTitle(title)) { + return res.status(400).json({ + success: false, + message: "Title must be a non-empty string" + }); + } + + if (completed !== undefined && !isValidCompleted(completed)) { + return res.status(400).json({ + success: false, + message: "Completed must be a boolean" + }); + } + + const updatedTodo = todoService.updateTodo(req.params.id, { + title, + completed + }); + + if (!updatedTodo) { + return res.status(404).json({ + success: false, + message: "Todo not found" + }); + } + + res.status(200).json({ + success: true, + message: "Todo updated successfully", + data: updatedTodo + }); +} + +function deleteTodo(req, res) { + const deletedTodo = todoService.deleteTodo(req.params.id); + + if (!deletedTodo) { + return res.status(404).json({ + success: false, + message: "Todo not found" + }); + } + + res.status(200).json({ + success: true, + message: "Todo deleted successfully", + data: deletedTodo + }); +} + +module.exports = { + getAllTodos, + getTodoById, + createTodo, + updateTodo, + deleteTodo +}; \ No newline at end of file diff --git a/backend/src/middleware/errorHandler.js b/backend/src/middleware/errorHandler.js new file mode 100644 index 00000000..ff69b9a3 --- /dev/null +++ b/backend/src/middleware/errorHandler.js @@ -0,0 +1,20 @@ +function notFoundHandler(req, res) { + res.status(404).json({ + success: false, + message: "Route not found" + }); +} + +function errorHandler(err, req, res, next) { + console.error(err); + + res.status(500).json({ + success: false, + message: "Internal server error" + }); +} + +module.exports = { + notFoundHandler, + errorHandler +}; \ No newline at end of file diff --git a/backend/src/routes/todoRoutes.js b/backend/src/routes/todoRoutes.js new file mode 100644 index 00000000..f8a25d40 --- /dev/null +++ b/backend/src/routes/todoRoutes.js @@ -0,0 +1,19 @@ +const express = require("express"); + +const { + getAllTodos, + getTodoById, + createTodo, + updateTodo, + deleteTodo +} = require("../controllers/todoController"); + +const router = express.Router(); + +router.get("/", getAllTodos); +router.get("/:id", getTodoById); +router.post("/", createTodo); +router.put("/:id", updateTodo); +router.delete("/:id", deleteTodo); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/server.js b/backend/src/server.js new file mode 100644 index 00000000..8fa43703 --- /dev/null +++ b/backend/src/server.js @@ -0,0 +1,7 @@ +const app = require("./app"); + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/backend/src/services/todoService.js b/backend/src/services/todoService.js new file mode 100644 index 00000000..84004b7c --- /dev/null +++ b/backend/src/services/todoService.js @@ -0,0 +1,61 @@ +let todos = []; +let nextId = 1; + +function getAllTodos() { + return todos; +} + +function getTodoById(id) { + return todos.find((todo) => todo.id === Number(id)); +} + +function createTodo(data) { + const newTodo = { + id: nextId++, + title: data.title.trim(), + completed: data.completed ?? false, + createdAt: new Date().toISOString() + }; + + todos.push(newTodo); + + return newTodo; +} + +function updateTodo(id, data) { + const todo = getTodoById(id); + + if (!todo) { + return null; + } + + if (data.title !== undefined) { + todo.title = data.title.trim(); + } + + if (data.completed !== undefined) { + todo.completed = data.completed; + } + + return todo; +} + +function deleteTodo(id) { + const todoIndex = todos.findIndex((todo) => todo.id === Number(id)); + + if (todoIndex === -1) { + return null; + } + + const deletedTodo = todos.splice(todoIndex, 1)[0]; + + return deletedTodo; +} + +module.exports = { + getAllTodos, + getTodoById, + createTodo, + updateTodo, + deleteTodo +}; \ No newline at end of file diff --git a/backend/src/utils/validators.js b/backend/src/utils/validators.js new file mode 100644 index 00000000..e7a7f654 --- /dev/null +++ b/backend/src/utils/validators.js @@ -0,0 +1,12 @@ +function isValidTitle(title) { + return typeof title === "string" && title.trim().length > 0; +} + +function isValidCompleted(completed) { + return typeof completed === "boolean"; +} + +module.exports = { + isValidTitle, + isValidCompleted +}; \ No newline at end of file