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
111 changes: 111 additions & 0 deletions backend/controller/todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const TodoModel = require("../model/todo");
const { validateCreate, validateUpdate } = require("../validation/todo");

// function for collecting data then convert into json
function parseBody(req) {
return new Promise((resolve, reject) => {
let raw = "";
req.on("data", (chunk) => (raw += chunk));
req.on("end", () => {
try {
resolve(raw ? JSON.parse(raw) : {});
}
catch {
reject({
status: 400,
message: "Invalid JSON"
});
}
});
});
}
// helper to send json responses back to client
function send(res, status, data) {
const body = JSON.stringify(data);
res.writeHead(status, {
"Content-Type": "application/json"
});
res.end(body);
}

const TodoController = {
// get all data
getAll(req, res) {
// response json
send(res, 200, {
success: true,
data: TodoModel.findAll()
});
},
// get single data
getOne(req, res, id) {
const todo = TodoModel.findById(id);
// if no todo then return
if (!todo) return send(res, 404, {
success: false,
error: `Todo ${id} not found`
});
// successfull json
send(res, 200, {
success: true,
data: todo });
},
// create a todo
async create(req, res) {
// get the parse json
const body = await parseBody(req);
// get the validation for create
const errors = validateCreate(body);

// check if has error
if (errors.length) return send(res, 400, {
success: false,
errors
});

// if success then create
const todo = TodoModel.create(body);
send(res, 201, {
success: true,
data: todo
});
},
// update a todo
async update(req, res, id) {
const body = await parseBody(req);
const errors = validateUpdate(body);
// check if has any error
if (errors.length) return send(res, 400, {
success: false,
errors
});
// if no id found
const todo = TodoModel.update(id, body);
if (!todo) return send(res, 404, {
success: false,
error: `Todo ${id} not found`
});
// if success then update
send(res, 200, {
success: true,
data: todo
});
},

// delete a todo
delete(req, res, id) {
const deleted = TodoModel.delete(id);
// if no id found
if (!deleted) return send(res, 404, {
success: false,
error: `Todo ${id} not found`
});
// if success then delete
send(res, 200, {
success: true,
message: `Todo ${id} deleted`
});
},
};
// export to use
module.exports = TodoController;
13 changes: 0 additions & 13 deletions backend/index.js

This file was deleted.

54 changes: 54 additions & 0 deletions backend/model/todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// local storage
let todos = [
{
id: 1,
title: "Buy groceries",
completed: false,
createdAt: new Date().toISOString()
},
{
id: 2,
title: "Read a book",
completed: true,
createdAt: new Date().toISOString()
},
];

let nextId = 3;

const TodoModel = {
// get all data
findAll() {
return todos;
},
// get by id
findById(id) {
return todos.find((t) => t.id === id) || null;
},
// create a todo
create({
title,
completed = false
}) {
const todo = { id: nextId++, title: title.trim(), completed, createdAt: new Date().toISOString() };
todos.push(todo);
return todo;
},
// update a todo
update(id, fields) {
const todo = todos.find((t) => t.id === id);
if (!todo) return null;
if (fields.title !== undefined) todo.title = fields.title.trim();
if (fields.completed !== undefined) todo.completed = fields.completed;
return todo;
},
// delete a todo
delete(id) {
const index = todos.findIndex((t) => t.id === id);
if (index === -1) return false;
todos.splice(index, 1);
return true;
},
};
// export
module.exports = TodoModel;
12 changes: 9 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
"name": "backend",
"version": "1.0.0",
"scripts": {
"start": "node index.js"
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
"express": "^4.22.1"
},
"description": "",
"main": "server.js",
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}
66 changes: 66 additions & 0 deletions backend/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
Api testing guide

server: http://localhost:3000

Endpoints:
| Method | Url | Description |
| Get | /api/todos | Get all data |
| Get | /api/todos/:id| Get single data ny id |
| Post | /api/todos | Create a todo | // must have json data including title and completed
| Put | /api/todos/:id| Update a single todo | // must have json data either title or completed
| Delete | /api/todos/:id| Delete a todo |

Sample json success data:
- Fetching all data
{
"success": true,
"data": [
{
"id": 1,
"title": "Buy groceries",
"completed": false,
"createdAt": "2026-02-17T03:47:54.454Z"
},
{
"id": 2,
"title": "Read a book",
"completed": true,
"createdAt": "2026-02-17T03:47:54.454Z"
}
]
}
- Get one Data
{
"success": true,
"data": {
"id": 1,
"title": "Buy groceries",
"completed": false,
"createdAt": "2026-02-17T03:47:54.454Z"
}
}
- Created todo
{
"success": true,
"data": {
"id": 3,
"title": "Create a new todo",
"completed": false,
"createdAt": "2026-02-17T05:05:56.386Z"
}
}
- Updated todo
{
"success": true,
"data": {
"id": 3,
"title": "Updated Title",
"completed": true,
"createdAt": "2026-02-17T05:05:56.386Z"
}
}
- Deleted todo
{
"success": true,
"message": "Todo 4 deleted"
}
28 changes: 28 additions & 0 deletions backend/routes/todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const TodoController = require("../controller/todo");

const TODO_LIST = /^\/api\/todos$/;
const TODO_ITEM = /^\/api\/todos\/(\d+)$/;

async function todoRouter(req, res) {
const method = req.method;
const url = req.url.split("?")[0];
const itemMatch = TODO_ITEM.exec(url);
const id = itemMatch ? parseInt(itemMatch[1]) : null;

// Collection: if url is equals to /api/todos
if (TODO_LIST.test(url)) {
if (method === "GET") return TodoController.getAll(req, res);
if (method === "POST") return TodoController.create(req, res);
}

// Item: if url is equals to /api/todos/:id
if (itemMatch) {
if (method === "GET") return TodoController.getOne(req, res, id);
if (method === "PUT") return TodoController.update(req, res, id);
if (method === "DELETE") return TodoController.delete(req, res, id);
}

return null; // no match
}
// exports
module.exports = todoRouter;
39 changes: 39 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const http = require("http");
const todoRouter = require("./routes/todo");

const PORT = process.env.PORT || 3000;

const server = http.createServer(async (req, res) => {
try {
const handled = await todoRouter(req, res);
if (handled === null) {
res.writeHead(404, {
"Content-Type": "application/json"
});
res.end(JSON.stringify({
success: false,
error: `Cannot ${req.method} ${req.url}`
}));
}
} catch (err) {
console.error(err);
res.writeHead(500, {
"Content-Type": "application/json"
});
res.end(JSON.stringify({
success: false,
error: "Internal server error"
}));
}
});

server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log("GET /api/todos");
console.log("GET /api/todos/:id");
console.log("POST /api/todos");
console.log("PUT /api/todos/:id");
console.log("DELETE /api/todos/:id");
});

module.exports = server;
30 changes: 30 additions & 0 deletions backend/validation/todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// validation for create
function validateCreate(body) {
const errors = [];

if (!body.title || typeof body.title !== "string" || !body.title.trim()) {
errors.push("'title' is required and must be a non-empty string");
}
if (body.completed !== undefined && typeof body.completed !== "boolean") {
errors.push("'completed' must be a boolean");
}
return errors;
}
// validation for update
function validateUpdate(body) {
const errors = [];

if (Object.keys(body).length === 0) {
errors.push("Body must include at least 'title' or 'completed'");
}
if (body.title !== undefined && (typeof body.title !== "string" || !body.title.trim())) {
errors.push("'title' must be a non-empty string");
}
if (body.completed !== undefined && typeof body.completed !== "boolean") {
errors.push("'completed' must be a boolean");
}

return errors;
}
// export
module.exports = { validateCreate, validateUpdate };