Skip to content

Expert · Performance and Node.js · topic 55 of 61 · 30 min

Files and a tiny server in Node

Explanation

node:fs/promises reads and writes files with async functions like readFile and writeFile, and path.join builds safe paths. node:http can start a web server in a few lines: a request handler receives the request and sends a response. For big files, streams process data piece by piece instead of loading it all into memory.

Reading and writing files

javascript
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import path from 'node:path';

const file = path.join('data', 'orders.json');
const text = await readFile(file, 'utf8');          // the 'utf8' gives a string, not a Buffer
const orders = JSON.parse(text);
orders.push({ id: 'OD1024', amount: 799 });
await mkdir('backup', { recursive: true });         // no error if it already exists
await writeFile(path.join('backup', 'orders.json'), JSON.stringify(orders, null, 2));

Use the promise-based functions (fs/promises) with await. Old code uses callback versions (fs.readFile(file, cb)) or synchronous ones (fs.readFileSync), which block the whole program while the disk works. Sync versions are fine in short scripts, never inside a server handling many users.

Careful: Never build file paths by joining user input directly: path.join('uploads', req.query.name) with name = "../../etc/passwd" escapes your folder. This is called path traversal. Use path.basename on user-supplied names, or check that the resolved path still starts with the folder you expect.

A tiny HTTP server

javascript
import http from 'node:http';

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true }));
    return;
  }
  res.writeHead(404, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: 'Not found' }));
});
server.listen(3000, () => console.log('Listening on http://localhost:3000'));

Every request calls the handler with req (method, url, headers, body stream) and res (where you write the status, headers and body). Frameworks like Express or Fastify add routing, JSON parsing and middleware on top of exactly this; the Backend Development course covers them.

Keep handlers testable

Put the real logic in a pure function that takes simple inputs (method, path, query) and returns { status, body }. The http handler just calls it and writes the result. Then you can test the logic without starting a server.

javascript
export function route(method, url) {
  if (method === 'GET' && url === '/health') return { status: 200, body: { ok: true } };
  return { status: 404, body: { error: 'Not found' } };
}
// in createServer: const { status, body } = route(req.method, req.url); …

EventEmitter

Many Node objects (servers, streams, process) are event emitters: they emit named events, and you listen with .on('event', handler). You can make your own with node:events.

javascript
import { EventEmitter } from 'node:events';
const orders = new EventEmitter();
orders.on('placed', (o) => console.log('Send SMS for', o.id));
orders.emit('placed', { id: 'OD1' });   // Send SMS for OD1

Streams for big data

readFile loads the whole file into memory: fine for 1 MB, terrible for a 5 GB log. A stream delivers data in chunks as it is read, so memory stays small. You can pipe streams together: read → transform → write.

javascript
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const lines = createInterface({ input: createReadStream('access.log') });
let errors = 0;
for await (const line of lines) if (line.includes(' 500 ')) errors++;
console.log('Server errors:', errors);
Think of it like this: readFile is like carrying a whole water tank home in one go. A stream is like a pipe connected to the tank: water arrives continuously, and you use it as it comes, no matter how big the tank is.
Tip: Always set Content-Type on responses and send JSON with JSON.stringify. Forgetting res.end() leaves the client waiting until it times out.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Files and a tiny server in Node · 1/6

Files and a server

In this lesson you will read and write files and start a small web server with Node.

Prefer a video by a teacher? Find free videos on “node js fs promises http createServer tutorial” on YouTube ↗

In real life: A daily backup script for a small clinic

A clinic's billing app saves the day's bills in a JSON file. Every night a Node script reads it, writes a dated copy into a backup folder, and logs how many bills were saved. It uses fs/promises, path.join and a date in the file name.

javascript
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import path from 'node:path';
const bills = JSON.parse(await readFile('bills.json', 'utf8'));
const day = new Date().toISOString().slice(0, 10);
await mkdir('backup', { recursive: true });
await writeFile(path.join('backup', `bills-${day}.json`), JSON.stringify(bills));
console.log(`Backed up ${bills.length} bills for ${day}`);

Practice (0/3 done)

Task 1 · Code · Javascript

Write the pure function route(method, url) for a tiny API: GET /health → { status: 200, body: { ok: true } }; GET /time → { status: 200, body: { iso: "2026-01-01T00:00:00.000Z" } } (fixed for the test); anything else → { status: 404, body: { error: "Not found" } }.

Task 2 · Code · Javascript

Write safeUploadPath(name) that returns "uploads/" + the file name with any folder parts removed (like path.basename), and null if the cleaned name is empty, "." or "..". This blocks path traversal.

Task 3 · Code · Javascript

Write countStatus(lines, code) that counts log lines (strings like 'GET /a 200 12ms') whose status field (the third space-separated part) equals code. It works on any iterable of lines, so the same function can consume a stream.

Quick quiz

Question 1

1.Why avoid fs.readFileSync inside a busy server?

Question 2

2.What does res.end() do?

Question 3

3.What is path traversal?

Question 4

4.Why use a stream for a 5 GB log file?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why avoid fs.readFileSync inside a busy server?

Answer: It blocks the whole process while reading

Sync calls stop the event loop for every user.

What does res.end() do?

Answer: Finishes the response so the client receives it

Without end, the client keeps waiting.

What is path traversal?

Answer: Using ../ in input to reach files outside the allowed folder

Never join raw user input into file paths.

Why use a stream for a 5 GB log file?

Answer: It processes chunks without loading the whole file into memory

Streams keep memory use small.

Why use http.Server with timeouts instead of http.ListenAndServe?Related

Answer: ListenAndServe has no timeouts, so slow clients can hold connections forever

Timeouts protect the server from slow or malicious clients.

From An HTTP server with net/http

In Express 5, what happens when an async route handler throws an error?Related

Answer: Express passes the error to the error-handling middleware automatically

Express 5 catches rejected promises from handlers and calls next(err) for you. Express 4 needed try/catch or a wrapper.

From Route handlers that follow the contract

node app.js hello: what is process.argv[2]?Related

Answer: "hello"

argv[0] is node, argv[1] is the script, argv[2] is the first argument.

From Node.js basics

Why can one Node.js process serve many users at once?Related

Answer: It does not wait idle for slow I/O; it serves other requests meanwhile

With non-blocking I/O, Node.js starts a slow job and serves other requests until the result is ready, like one waiter serving many tables.

From Node.js: JavaScript on the server

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Performance and Node.js” before its exam: Real project tooling.
  2. Module 11 exam: Performance and Node.js · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Interview preparation and career. 61 topics left in the course before the final exam.

← Previous: Node.js basics