Init
This commit is contained in:
13
index.js
Normal file
13
index.js
Normal 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
11
package.json
Normal 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"
|
||||||
|
}
|
111
src/controllers/todoController.js
Normal file
111
src/controllers/todoController.js
Normal 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
72
src/data/todos.js
Normal 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
83
src/server.js
Normal 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
53
src/utils/helpers.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
Reference in New Issue
Block a user