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
43 changes: 43 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Form data</title>
</head>
<body>
<h1>Form data for expense</h1>
<form action="/add-expense" method="POST">
<label for="date">Date:</label><br>
<input
type="date"
name="date"
id="date"
required
/>

<br><br>

<label for="title">Title:</label><br>
<input
id="title"
name="title"
type="text"
required
/>

<br><br>

<label for="amount">Amount:</label><br>
<input
type="number"
name="amount"
id="amount"
required
/>

<br><br>

<button type="submit">Send request</button>
</form>
</body>
</html>
122 changes: 120 additions & 2 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,126 @@
'use strict';

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

function createServer() {
/* Write your code here */
// Return instance of http.Server class
const server = http.createServer((req, res) => {
const reqUrl = new URL(req.url || '', `http://${req.headers.host}`);
const pathname = reqUrl.pathname;

if (req.method === 'GET' && pathname === '/') {
const indexPath = path.resolve('public', 'index.html');

res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
fs.createReadStream(indexPath).pipe(res);

return;
}

if (req.method === 'GET' && pathname === '/add-expense') {
res.statusCode = 400;

return res.end('Only POST method allowed');
}

if (req.method === 'POST' && pathname === '/add-expense') {
let body = '';

req.on('data', (chunk) => {
body += chunk.toString();
});

req.on('end', () => {
try {
const contentType = req.headers['content-type'];
let dataObj;

if (contentType.includes('application/json')) {
dataObj = JSON.parse(body);
} else if (
contentType.includes('application/x-www-form-urlencoded')
) {
const parsed = new URLSearchParams(body);

dataObj = Object.fromEntries(parsed.entries());
} else {
res.writeHead(400, { 'Content-Type': 'text/plain' });

return res.end('Unsupported content type');
}

const { date, title, amount } = dataObj;

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

return res.end('Invalid data format');
Comment on lines +56 to +59

Choose a reason for hiding this comment

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

The code checks for presence of amount, but does not validate that it is numeric. The requirements specify that non-numeric amounts should be handled as invalid input. Consider adding a check like isNaN(Number(amount)) and returning a 400 error if the value is not numeric.

}

const dbDir = path.resolve('db');

if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}

const filePath = path.join(dbDir, 'expense.json');

fs.readFile(filePath, (err, fileData) => {
let newDataForFile;

if (!err && fileData.length > 0) {
try {
const temp = JSON.parse(fileData);

if (Array.isArray(temp)) {
temp.push(dataObj);
newDataForFile = temp;
} else if (
typeof temp === 'object' &&
temp !== null &&
Object.keys(temp).length > 0
) {
newDataForFile = [temp, dataObj];
} else {
newDataForFile = dataObj;
}
} catch (e) {
newDataForFile = dataObj;
}
} else {
newDataForFile = dataObj;
Comment on lines +87 to +93

Choose a reason for hiding this comment

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

When the file is empty, invalid, or not an array, the code sets newDataForFile = dataObj; (lines 87, 90, 93). This means a single object will be written to expense.json, but the requirements specify that the file must always contain an array of expense objects. You should always write an array (e.g., [dataObj] or by appending to an existing array).

}

fs.writeFile(
filePath,
JSON.stringify(newDataForFile, null, 2),
(error) => {
if (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' });

return res.end(`Server error: ${error}`);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(dataObj));
Comment on lines +105 to +106

Choose a reason for hiding this comment

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

After saving, the response is sent with Content-Type: application/json and the raw JSON data. The requirements specify that you must return an HTML page that displays the saved JSON in a well-formatted way (e.g., inside a <pre> tag), and set the content type to text/html. Please update the response to generate an HTML page with the pretty-printed JSON.

},
);
});
} catch (e) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end(`Invalid request body: ${e.message}`);
}
});

return;
}

res.statusCode = 404;
res.end('Not found');
});

return server;
}

module.exports = {
Expand Down