From 6296986e69e808de90dca2e7e5a350ee139ba7e2 Mon Sep 17 00:00:00 2001 From: Mykhailo Date: Wed, 10 Sep 2025 16:33:34 +0300 Subject: [PATCH 1/3] feat: implement form data --- src/app.js | 5 +- src/createServer.js | 120 +++++++++++++++++++++++++++++++++++++++++++- src/index.html | 28 +++++++++++ 3 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 src/index.html diff --git a/src/app.js b/src/app.js index 6e754b2..f0c515a 100644 --- a/src/app.js +++ b/src/app.js @@ -3,7 +3,4 @@ const { createServer } = require('./createServer'); -createServer().listen(5701, () => { - console.log(`Server is running on http://localhost:${5701} 🚀`); - console.log('Available at http://localhost:5701'); -}); +createServer().listen(5701); diff --git a/src/createServer.js b/src/createServer.js index 1cf1dda..4cc2ec8 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,8 +1,124 @@ 'use strict'; +/* eslint-disable no-console */ + +const { createReadStream, createWriteStream } = require('fs'); +const http = require('http'); +const path = require('path'); + function createServer() { - /* Write your code here */ - // Return instance of http.Server class + const server = new http.Server(); + + function getHandler(req, res) { + if (req.method !== 'GET') { + return; + } + + if (req.url !== '/' && req.url !== '') { + res.statusCode = 404; + res.setHeader('Content-Type', 'text/plain'); + res.end('Not Found'); + + return; + } + + const form = createReadStream(path.join(__dirname, 'index.html')); + + res.on('close', () => { + form.destroy(); + }); + + form.pipe(res); + } + + function postHandler(req, res) { + if (req.method !== 'POST') { + return; + } + + if (req.url !== '/add-expense') { + res.statusCode = 404; + res.setHeader('Content-Type', 'text/plain'); + res.end('Not Found'); + + return; + } + + const file = path.resolve('db', 'expense.json'); + + let raw = ''; + + req.setEncoding('utf8'); + + req.on('data', (chunk) => { + raw += chunk; + }); + + req.on('end', () => { + let payload; + + try { + payload = raw ? JSON.parse(raw) : {}; + } catch (parseErr) { + res.statusCode = 400; + res.setHeader('Content-Type', 'text/plain'); + res.end('Bad Request'); + + return; + } + + const expense = { + date: payload.date, + title: payload.title, + amount: payload.amount, + }; + + if (!expense.date || !expense.title || !expense.amount) { + res.statusCode = 400; + res.setHeader('Content-Type', 'text/plain'); + res.end('Missing required fields'); + + return; + } + + const writer = createWriteStream(file); + + writer.on('error', (error) => { + console.error('Write stream error:', error); + res.statusCode = 500; + res.setHeader('Content-Type', 'text/plain'); + res.end('Internal Server Error'); + }); + + writer.end(JSON.stringify(expense), () => { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(expense)); + }); + }); + + req.on('error', (err) => { + console.error('Request error:', err); + res.statusCode = 400; + res.setHeader('Content-Type', 'text/plain'); + res.end('Bad Request'); + }); + } + + server.on('request', getHandler); + server.on('request', postHandler); + + server.on('request', (req, res) => { + if (req.method !== 'GET' && req.method !== 'POST') { + res.statusCode = 405; + res.setHeader('Content-Type', 'text/plain'); + res.end('Method Not Allowed'); + } + }); + + server.on('error', () => {}); + + return server; } module.exports = { diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..35ed478 --- /dev/null +++ b/src/index.html @@ -0,0 +1,28 @@ + + + + + + Document + + +
+ +

+ + +

+ + +

+ + +
+ + From 3ed0216e30e7889bf35b1e609297871930128a70 Mon Sep 17 00:00:00 2001 From: Mykhailo Date: Wed, 10 Sep 2025 16:47:16 +0300 Subject: [PATCH 2/3] fix: fix createServer issues --- src/createServer.js | 81 ++++++++++++++++++++++++++++++++++++++++----- src/index.html | 2 +- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/createServer.js b/src/createServer.js index 4cc2ec8..15fc3ab 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -2,7 +2,7 @@ /* eslint-disable no-console */ -const { createReadStream, createWriteStream } = require('fs'); +const { createReadStream, createWriteStream, mkdirSync } = require('fs'); const http = require('http'); const path = require('path'); @@ -36,7 +36,7 @@ function createServer() { return; } - if (req.url !== '/add-expense') { + if (req.url !== '/add-expense' && req.url !== '/submit-expense') { res.statusCode = 404; res.setHeader('Content-Type', 'text/plain'); res.end('Not Found'); @@ -55,10 +55,19 @@ function createServer() { }); req.on('end', () => { + const contentType = req.headers['content-type'] || ''; let payload; try { - payload = raw ? JSON.parse(raw) : {}; + if (contentType.includes('application/json')) { + payload = raw ? JSON.parse(raw) : {}; + } else if (contentType.includes('application/x-www-form-urlencoded')) { + const params = new URLSearchParams(raw); + + payload = Object.fromEntries(params.entries()); + } else { + payload = {}; + } } catch (parseErr) { res.statusCode = 400; res.setHeader('Content-Type', 'text/plain'); @@ -73,10 +82,48 @@ function createServer() { amount: payload.amount, }; - if (!expense.date || !expense.title || !expense.amount) { - res.statusCode = 400; + // Validation + const parsedAmount = + typeof expense.amount === 'string' + ? parseFloat(expense.amount) + : Number(expense.amount); + const parsedDate = new Date(expense.date); + + const hasAll = Boolean(expense.date && expense.title && expense.amount); + const amountValid = + !Number.isNaN(parsedAmount) && Number.isFinite(parsedAmount); + const dateValid = !Number.isNaN(parsedDate.getTime()); + + if (!hasAll || !amountValid || !dateValid) { + if (contentType.includes('application/json')) { + res.statusCode = 400; + res.setHeader('Content-Type', 'text/plain'); + res.end('Invalid payload'); + } else { + res.statusCode = 400; + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + + res.end( + `Error

Invalid input

Please provide a valid date, title, and numeric amount.

`, + ); + } + + return; + } + + const normalizedExpense = { + date: expense.date, + title: expense.title, + amount: String(expense.amount), + }; + + try { + mkdirSync(path.dirname(file), { recursive: true }); + } catch (mkdirErr) { + console.error('Directory create error:', mkdirErr); + res.statusCode = 500; res.setHeader('Content-Type', 'text/plain'); - res.end('Missing required fields'); + res.end('Internal Server Error'); return; } @@ -90,10 +137,26 @@ function createServer() { res.end('Internal Server Error'); }); - writer.end(JSON.stringify(expense), () => { + const pretty = JSON.stringify(normalizedExpense, null, 2); + + writer.end(pretty, () => { res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(expense)); + + if (contentType.includes('application/json')) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(normalizedExpense)); + } else { + const escaped = pretty + .replace(/&/g, '&') + .replace(//g, '>'); + + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + + res.end( + `Saved

Expense saved

${escaped}
`, + ); + } }); }); diff --git a/src/index.html b/src/index.html index 35ed478..f5e408f 100644 --- a/src/index.html +++ b/src/index.html @@ -6,7 +6,7 @@ Document -
+

From 03f3513ef81a55b6421e2f017c7e1db13b033c1d Mon Sep 17 00:00:00 2001 From: Mykhailo Date: Wed, 10 Sep 2025 16:54:29 +0300 Subject: [PATCH 3/3] refactor: set content-type before piping the form --- src/createServer.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/createServer.js b/src/createServer.js index 15fc3ab..af88f5d 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -22,8 +22,24 @@ function createServer() { return; } + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + const form = createReadStream(path.join(__dirname, 'index.html')); + form.on('error', (err) => { + console.error('Read stream error:', err); + + if (!res.headersSent) { + res.statusCode = 500; + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + + res.end(); + } else { + res.destroy(); + } + }); + res.on('close', () => { form.destroy(); });