This commit is contained in:
2025-06-09 12:10:02 -06:00
parent 18264ef7cf
commit a88f6faa74
6 changed files with 343 additions and 0 deletions

13
index.js Normal file
View File

@ -0,0 +1,13 @@
import server from "./src/server.js";
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || "127.0.0.1";
server.listen(PORT, HOST, () => {
console.log(`Todo API running at http://${HOST}:${PORT}/`);
console.log("Available endpoints:");
console.log(" GET /todos - Get all todos");
console.log(" POST /todos - Create todo");
console.log(" PUT /todos/:id - Update todo");
console.log(" DELETE /todos/:id - Delete todo");
});

11
package.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "nodeapi-template",
"version": "1.0.0",
"description": "Basic node.js API template",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
},
"type": "module"
}

View File

@ -0,0 +1,111 @@
import { sendJSON, parseBody } from "../utils/helpers.js";
import {
getTodos,
addTodo,
updateTodoById,
deleteTodoById,
} from "../data/todos.js";
/**
* @typedef {Object} Todo
* @property {number} id - Todo unique identifier
* @property {string} text - Todo text content
* @property {boolean} completed - Todo completion status
*/
/**
* @typedef {Object} CreateTodoRequest
* @property {string} text - Todo text content
*/
/**
* @typedef {Object} UpdateTodoRequest
* @property {string} [text] - Todo text content
* @property {boolean} [completed] - Todo completion status
*/
/**
* @typedef {Object} RequestWithParams
* @property {Object} params - Request parameters
* @property {string} params.id - Todo ID from URL
*/
/**
* Get all todos
* @param {import('http').IncomingMessage} req - HTTP request object
* @param {import('http').ServerResponse} res - HTTP response object
* @returns {void}
*/
export const getAllTodos = (req, res) => {
/** @type {Todo[]} */
const todos = getTodos();
sendJSON(res, 200, todos);
};
/**
* Create a new todo
* @param {import('http').IncomingMessage} req - HTTP request object
* @param {import('http').ServerResponse} res - HTTP response object
* @returns {void}
*/
export const createTodo = (req, res) => {
parseBody(req, (error, data) => {
if (error) {
return sendJSON(res, 400, { error: "Invalid JSON" });
}
/** @type {CreateTodoRequest} */
const todoData = data;
if (!todoData.text) {
return sendJSON(res, 400, { error: "Text is required" });
}
/** @type {Todo} */
const newTodo = addTodo(todoData.text);
sendJSON(res, 201, newTodo);
});
};
/**
* Update an existing todo
* @param {import('http').IncomingMessage & RequestWithParams} req - HTTP request object with params
* @param {import('http').ServerResponse} res - HTTP response object
* @returns {void}
*/
export const updateTodo = (req, res) => {
parseBody(req, (error, data) => {
if (error) {
return sendJSON(res, 400, { error: "Invalid JSON" });
}
/** @type {UpdateTodoRequest} */
const updateData = data;
/** @type {Todo | null} */
const updatedTodo = updateTodoById(req.params.id, updateData);
if (updatedTodo) {
sendJSON(res, 200, updatedTodo);
} else {
sendJSON(res, 404, { error: "Todo not found" });
}
});
};
/**
* Delete a todo
* @param {import('http').IncomingMessage & RequestWithParams} req - HTTP request object with params
* @param {import('http').ServerResponse} res - HTTP response object
* @returns {void}
*/
export const deleteTodo = (req, res) => {
/** @type {boolean} */
const deleted = deleteTodoById(req.params.id);
if (deleted) {
sendJSON(res, 200, { message: "Todo deleted" });
} else {
sendJSON(res, 404, { error: "Todo not found" });
}
};

72
src/data/todos.js Normal file
View File

@ -0,0 +1,72 @@
/**
* @typedef {Object} Todo
* @property {number} id - Todo unique identifier
* @property {string} text - Todo text content
* @property {boolean} completed - Todo completion status
*/
/**
* @typedef {Object} TodoUpdate
* @property {string} [text] - Todo text content
* @property {boolean} [completed] - Todo completion status
*/
/**
* In-memory storage for todos
* @type {Todo[]}
*/
let todos = [
{ id: 1, text: "Learn Node.js", completed: false },
{ id: 2, text: "Build an API", completed: true },
];
/**
* Get all todos
* @returns {Todo[]} Array of all todos
*/
export const getTodos = () => todos;
/**
* Add a new todo
* @param {string} text - Todo text content
* @returns {Todo} The newly created todo
*/
export const addTodo = (text) => {
/** @type {Todo} */
const newTodo = {
id: todos.length > 0 ? Math.max(...todos.map((t) => t.id)) + 1 : 1,
text,
completed: false,
};
todos.push(newTodo);
return newTodo;
};
/**
* Update a todo by ID
* @param {string} id - Todo ID to update
* @param {TodoUpdate} updates - Properties to update
* @returns {Todo | null} Updated todo or null if not found
*/
export const updateTodoById = (id, updates) => {
/** @type {number} */
const todoIndex = todos.findIndex((todo) => todo.id === parseInt(id));
if (todoIndex === -1) return null;
todos[todoIndex] = { ...todos[todoIndex], ...updates };
return todos[todoIndex];
};
/**
* Delete a todo by ID
* @param {string} id - Todo ID to delete
* @returns {boolean} True if deleted, false if not found
*/
export const deleteTodoById = (id) => {
/** @type {number} */
const todoIndex = todos.findIndex((todo) => todo.id === parseInt(id));
if (todoIndex === -1) return false;
todos.splice(todoIndex, 1);
return true;
};

83
src/server.js Normal file
View File

@ -0,0 +1,83 @@
import http from "node:http";
import url from "node:url";
import { sendJSON, setCorsHeaders } from "./utils/helpers.js";
import {
getAllTodos,
createTodo,
updateTodo,
deleteTodo,
} from "./controllers/todoController.js";
/**
* @typedef {Object} ApiEndpoints
* @property {string} "GET /todos" - Get all todos
* @property {string} "POST /todos" - Create todo
* @property {string} "PUT /todos/:id" - Update todo
* @property {string} "DELETE /todos/:id" - Delete todo
*/
/**
* @typedef {Object} RootResponse
* @property {string} message - API name
* @property {string} version - API version
* @property {ApiEndpoints} endpoints - Available endpoints
*/
/**
* @typedef {Object} ErrorResponse
* @property {string} error - Error message
*/
/**
* HTTP server that handles Todo API requests
* @type {http.Server}
*/
const server = http.createServer((req, res) => {
/** @type {url.UrlWithParsedQuery} */
const parsedUrl = url.parse(req.url, true);
/** @type {string} */
const path = parsedUrl.pathname;
/** @type {string} */
const method = req.method;
setCorsHeaders(res);
if (method === "OPTIONS") {
res.statusCode = 200;
res.end();
return;
}
// Routes
if (path === "/" && method === "GET") {
// Root route
sendJSON(res, 200, {
message: "Todo API",
version: "1.0.0",
endpoints: {
"GET /todos": "Get all todos",
"POST /todos": "Create todo",
"PUT /todos/:id": "Update todo",
"DELETE /todos/:id": "Delete todo",
},
});
} else if (path === "/todos" && method === "GET") {
getAllTodos(req, res);
} else if (path === "/todos" && method === "POST") {
createTodo(req, res);
} else if (path.match(/^\/todos\/\d+$/) && method === "PUT") {
/** @type {string} */
const id = path.split("/")[2];
req.params = { id };
updateTodo(req, res);
} else if (path.match(/^\/todos\/\d+$/) && method === "DELETE") {
/** @type {string} */
const id = path.split("/")[2];
req.params = { id };
deleteTodo(req, res);
} else {
sendJSON(res, 404, { error: "Route not found" });
}
});
export default server;

53
src/utils/helpers.js Normal file
View File

@ -0,0 +1,53 @@
/**
* @callback ParseBodyCallback
* @param {Error | null} error - Parse error if any
* @param {Object} data - Parsed JSON data
* @returns {void}
*/
/**
* Set CORS headers on response
* @param {import('http').ServerResponse} res - HTTP response object
* @returns {void}
*/
export const setCorsHeaders = (res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
};
/**
* Send JSON response
* @param {import('http').ServerResponse} res - HTTP response object
* @param {number} statusCode - HTTP status code
* @param {Object | Array} data - Data to send as JSON
* @returns {void}
*/
export const sendJSON = (res, statusCode, data) => {
res.statusCode = statusCode;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(data));
};
/**
* Parse request body as JSON
* @param {import('http').IncomingMessage} req - HTTP request object
* @param {ParseBodyCallback} callback - Callback function with parsed data or error
* @returns {void}
*/
export const parseBody = (req, callback) => {
/** @type {string} */
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
/** @type {Object} */
const data = body ? JSON.parse(body) : {};
callback(null, data);
} catch (error) {
callback(error);
}
});
};