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
75 changes: 70 additions & 5 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,75 @@
'use strict';

const http = require('http');
const fs = require('fs');
const path = require('path');

const DATA_PATH = path.resolve('db', 'expense.json');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DATA_PATH is db/expense.json but the code doesn't ensure the db directory exists. If db/ is missing, fs.writeFileSync will throw. Before writing, create the directory if needed, e.g. fs.mkdirSync(path.dirname(DATA_PATH), { recursive: true }). This change ensures saving to db/expense.json always succeeds.


function createServer() {
/* Write your code here */
// Return instance of http.Server class
return http.createServer((req, res) => {
const indexPath = path.resolve('src', 'index.html');


if (req.method === 'GET' && req.url === '/') {
try {
const file = fs.readFileSync(indexPath);

res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(file);
Comment on lines +16 to +19

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serving src/index.html on GET / is implemented correctly (reads file and sends with text/html). Keep this, but after fixing endpoints and save behavior re-test full flow: load page -> submit form -> server saves to db/expense.json (array) -> server returns an HTML page with pretty JSON. Good work on this part.

} catch {
res.writeHead(404, { 'Content-Type': 'text/plain' });


Comment on lines +20 to +23

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch after reading the index file sets a 404 header but never sends a body or returns. This leaves the request flow in an invalid state and allows the body-parsing logic below to execute inside the GET '/' handler. You must close the catch properly (send a response body like res.end('Not found')) and return so subsequent code doesn't run inside this branch. This is a critical syntax/flow bug that must be fixed.

const chunks = [];

req.on('data', (chunk) => chunks.push(chunk));

req.on('end', () => {
try {
const buffer = Buffer.concat(chunks).toString();
const obj = JSON.parse(buffer);
Comment on lines +24 to +31

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Body-parsing code lives here inside the GET / branch (collecting data events and parsing JSON). It must be executed only for POST requests. Create a separate POST route and move this logic there. Also ensure you check the request URL (client posts to /submit-expense) so the server handles the correct endpoint.


if (!obj.date || !obj.title || !obj.amount) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid expense data');

return;
}
Comment on lines +33 to +38

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation for required fields (date, title, amount) is present — good — but because this block is currently unreachable it never runs. Keep this validation in the POST handler, and when validation fails return an HTML response (not plain text) or at least an HTML error page so responses remain consistent with the requirement that POST returns HTML containing JSON.

Comment on lines +30 to +38

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You parse the incoming body with JSON.parse(buffer) but there's no coercion/validation of amount. Per the requirements validate date, title, and amount and coerce amount to a Number (const amount = Number(obj.amount)) and confirm !Number.isNaN(amount). If validation fails, return an HTML error response with a 400 status. Right now only presence checks are used which is insufficient.



fs.writeFileSync(DATA_PATH, JSON.stringify(obj, null, 2));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code currently writes the new expense object directly to db/expense.json (overwriting it). The client expects an array of expenses when rendering (it calls res.json() and iterates/prints as an array). To support multiple expenses, read existing content (if any) into an array, push the new expense, then write the updated array back with JSON.stringify(array, null, 2). This prevents losing previous entries and matches the client's expectation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line overwrites db/expense.json with a single object. The spec requires saving submitted expense data to db/expense.json and preserving previous entries. Instead, read the existing file (fallback to an empty array []), push the new expense, and write back JSON.stringify(array, null, 2). Also ensure the db directory exists before writing (e.g., fs.mkdirSync(path.dirname(DATA_PATH), { recursive: true })) to avoid first-run errors.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You call fs.writeFileSync(DATA_PATH, JSON.stringify(obj, null, 2)), which overwrites the file with a single object. The app must persist expenses as an array. Read the existing file (fallback to []), push the new expense, ensure the db directory exists (fs.mkdirSync(path.dirname(DATA_PATH), { recursive: true })), then write back JSON.stringify(array, null, 2). This ensures previous expenses are preserved.


res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(obj));
Comment on lines +43 to +44

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After successfully saving the expense the server responds with Content-Type application/json and raw JSON. The task requires returning an HTML page with well-formatted (pretty-printed) JSON embedded (for example inside a <pre>). Update this response to Content-Type: text/html and send an HTML document that contains JSON.stringify(obj, null, 2) inside a <pre> so it is human-readable.

Comment on lines +43 to +44

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After save the server responds with Content-Type: application/json. The requirement states the response after form submission must be an HTML page containing well-formatted (pretty-printed) JSON. Change this response to send an HTML document (for example, res.writeHead(200, { 'Content-Type': 'text/html' }) and include <pre>${JSON.stringify(obj, null, 2)}</pre> in the body) so the client receives HTML with pretty-printed JSON.

Comment on lines +43 to +44

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After saving you respond with Content-Type: 'application/json' and return raw JSON. The task requires returning an HTML page containing pretty-printed JSON. Update the response to Content-Type: 'text/html' and send an HTML body containing the formatted JSON, e.g. <pre>${JSON.stringify(savedObj, null, 2)}</pre>.

} catch {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid JSON');
}
});
Comment on lines +24 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code that collects chunks, parses the request body and writes the expense (lines 24–49) is currently placed inside the GET '/' handler. Move this whole block into a dedicated POST handler such as if (req.method === 'POST' && req.url === '/submit-expense') { ... }. The POST handler should only run for POST requests and the GET '/' handler should only serve the HTML form. This fixes the routing and makes the server behave as required.


return;
}
if (req.method === 'GET' && req.url === '/expenses') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GET endpoint serving saved data is /expenses and it returns JSON. However the client attempts to fetch /db/expense.json to load all expenses (see src/index.html). Either serve the file at /db/expense.json or change the client to request /expenses. Also ensure the returned structure is an array (empty array [] when no data) rather than an empty object {}, so JSON.stringify on the client renders a list correctly.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Endpoint mismatch: the client (src/index.html) posts to /submit-expense and fetches /db/expense.json to display expenses. The server currently exposes GET /expenses. Either change the client to fetch /expenses and post to /submit-expense, or make the server serve db/expense.json at /db/expense.json and implement POST /submit-expense. Pick one consistent routing approach and implement it. See client for current endpoints: .

try {
const data = fs.existsSync(DATA_PATH)
? fs.readFileSync(DATA_PATH, 'utf-8')
: '';
const obj = data ? JSON.parse(data) : {};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor but important: when reading existing file you parse into obj and fall back to {} when no data. For consistency with the client and multiple expenses you should use an array fallback: const arr = data ? JSON.parse(data) : []; and then res.end(JSON.stringify(arr, null, 2));. This makes the API predictable for the frontend.

Comment on lines +55 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file-read fallback uses an empty string and then returns {} when no data exists (const data = ... ? fs.readFileSync(...) : ''; const obj = data ? JSON.parse(data) : {};). Prefer returning [] (and parse to an array) to match the expected stored type of multiple expenses. Also handle JSON parse errors more explicitly so the server doesn't crash on corrupt file contents.

Comment on lines +55 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GET /expenses handler returns {} when the file is missing (const obj = data ? JSON.parse(data) : {}). For an expenses list endpoint you should return an empty array [] when there are no saved expenses. Also add defensive handling for invalid/corrupt JSON (return [] or a 500 with a clear message).


res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(obj));
Comment on lines +53 to +61

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GET handler for /expenses reads the file and returns parsed data or {} when there's no data. If you store expenses as an array (recommended), return an empty array [] when the file doesn't exist so the client receives a consistent array type. Also note: the client currently fetches /db/expense.json (in index.html), but this server serves /expenses — align these endpoints (either serve /db/expense.json or change the client to fetch /expenses).

} catch {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Failed to read expense');
}

return;
}

res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
});
}

module.exports = {
createServer,
};
module.exports = { createServer };
55 changes: 55 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Add Expense</title>
</head>
<body>
<h1>Add Expense</h1>
<form id="expense-form">
<label>Title: <input type="text" name="title" required></label><br>
<label>Date: <input type="date" name="date" required></label><br>
<label>Amount: <input type="number" name="amount" required></label><br>
<button type="submit">Add Expense</button>
</form>

<h2>All Expenses</h2>
<div id="expenses"></div>

<script>
const form = document.getElementById('expense-form');
const expensesDiv = document.getElementById('expenses');

async function loadExpenses() {
try {
const res = await fetch('/db/expense.json');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client attempts to load saved expenses from /db/expense.json with fetch('/db/expense.json'). The server in the project exposes a GET /expenses endpoint and does not serve the db directory statically. Either change this fetch to /expenses or make the server serve /db/expense.json explicitly. As-is, this fetch will fail. See server implementation for current endpoint names.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client fetches saved expenses from /db/expense.json (used in loadExpenses). Ensure the server serves that path or change the client to fetch the server endpoint that provides stored expenses (for example /expenses). Right now this path may not exist on the server, so the client will show "No expenses yet." if it can't fetch the file.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client attempts to load saved expenses from /db/expense.json. Ensure your server actually serves that path (or change this client fetch to match the server endpoint). The current server implementation in the project listens for /expenses (not /db/expense.json) which leads to a mismatch; align them so the fetch succeeds. See server file for reference .

const arr = await res.json();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You call await res.json() without checking the response status. If the file is missing the fetch may return 404 and res.json() will throw. Consider checking if (!res.ok) throw new Error('No expenses') before parsing, or handle res.status explicitly so the UI can show a friendly message rather than relying on the catch block alone.

expensesDiv.innerHTML = `<pre>${JSON.stringify(arr, null, 2)}</pre>`;
Comment on lines +26 to +27

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client expects an array of expenses (const arr = await res.json(); and then JSON.stringify(arr, null, 2)). The server currently writes a single object to db/expense.json in its POST handler (no aggregation). Decide whether the file should hold an array (and append new expenses) or a single object. For the client to display a list, prefer storing an array and updating it on each POST (read existing array, push new entry, write back with JSON.stringify(array, null, 2)).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Displaying expenses with JSON.stringify(arr, null, 2) inside a <pre> is correct and produces the required pretty-printed JSON for the UI. Keep this as-is once the server writes an array to db/expense.json.

} catch {
expensesDiv.innerHTML = 'No expenses yet.';
Comment on lines +24 to +29

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider error handling and UX: loadExpenses() shows 'No expenses yet.' only on fetch failure. If the server responds with an empty file or a single object instead of an array, res.json() may throw or produce unexpected results. Make sure the server returns valid JSON arrays or the client handles single-object responses appropriately.

}
}

form.addEventListener('submit', async e => {
e.preventDefault();

const data = {
title: form.title.value,
date: form.date.value,
amount: form.amount.value
Comment on lines +36 to +39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The form correctly includes the required fields title, date, and amount, satisfying the form field requirement from the task description. Make sure to preserve these names because the server will parse them (if you keep JSON submission).

Comment on lines +36 to +39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapping of form inputs to the data object (title, date, amount) is correct and matches the required fields. Consider whether amount should be converted to a Number (Number(form.amount.value)) before sending or handle parsing on the server; either is acceptable as long as the server-side storage is consistent.

Comment on lines +36 to +39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When building the data object you send amount as a string. Coerce it to a Number before sending (e.g. amount: Number(form.amount.value)) and validate it (ensure it's not NaN) — this simplifies server-side validation and meets the task requirement to treat amount numerically.

};

await fetch('/submit-expense', {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client posts to /submit-expense (fetch('/submit-expense', ...)) but the server listens for POST at /add-expense. These endpoints must match. Update the client to POST to /add-expense or change the server to accept /submit-expense so the POST reaches the handler. This mismatch prevents the server from receiving submitted expenses.

method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
Comment on lines +44 to +45

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You send JSON from the client with Content-Type: application/json and the server currently parses JSON — that is OK if you intend to use JSON. Just ensure both sides agree on JSON and the URL. If you prefer a normal HTML form POST (no JS), you'd need to change client and server to use application/x-www-form-urlencoded parsing.

});
Comment on lines +42 to +46

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The form is submitted with fetch('/submit-expense', { method: 'POST', ... }). Ensure the server exposes a matching POST /submit-expense route. If the server uses a different route (e.g. /add-expense or /expenses), change one side so they match; otherwise the POST will never reach the server handler. Also confirm the server parses application/json request bodies.

Comment on lines +42 to +46

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You await fetch('/submit-expense', ...) but do not check the response. The server is required to return an HTML page with formatted JSON after saving; consider reading the response text (const text = await res.text()) and displaying it or handling non-OK statuses (if (!res.ok) { /* show error */ }). Also ensure your server actually implements POST /submit-expense and responds with Content-Type: text/html containing pretty-printed JSON per the task description .


form.reset();
loadExpenses();
Comment on lines +46 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the POST you await fetch(...) but ignore the response and then call loadExpenses() which fetches the stored file. The task requires that after saving the app "return an HTML page with well formatted JSON." Currently your client ignores any HTML response from the server and expects to read JSON from a file. To satisfy the requirement, the server should respond to the POST with an HTML page (Content-Type text/html) containing pretty-printed JSON (e.g. inside a <pre>). The client should then display that HTML (or navigate to it) rather than just calling loadExpenses(). See the task description about returning an HTML page after saving.

Comment on lines +46 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the fetch POST completes the code calls loadExpenses() to refresh the shown list. This is sensible, but note: the requirement states the server must "return an HTML page with well formatted JSON" after form submission. Because the client sends the form via fetch, the browser will not automatically navigate to or display that HTML response. Decide which behavior you want: (a) keep fetch and let the server return HTML (tests can still validate the response), and rely on loadExpenses() to refresh the list; or (b) submit the form as a normal form (no fetch) so the browser navigates to the HTML response returned by the server. Make sure chosen approach aligns with automated tests/requirements.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After submitting you immediately call loadExpenses() which re-fetches the JSON file. That's fine, but if the server returns the required HTML response for the POST it will be ignored. Decide whether you want to show the server's returned HTML to the user (recommended for debugging/requirements compliance) or rely only on reloading the JSON file. Either way, handle errors from the POST request and show feedback to the user.

});

loadExpenses();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: you call loadExpenses() on page load which is good. After you align endpoints and response formats, test the full flow: load page, submit form, server saves to db/expense.json (ensure db exists), and the app displays the HTML response with pretty JSON or loads the /expenses JSON array as intended.

</script>
</body>
</html>
Loading