Sitemap

JavaScript + Backend Bootcamp

Day 10 : Node.js Core Modules + HTTP Server

--

Today you’ll learn how Node.js works with:

  • Files and directories
  • Paths
  • Operating-system information
  • Events
  • Buffers
  • Streams
  • HTTP requests and responses
  • Routes
  • Status codes
  • JSON APIs

You’ll also build:

  • A TypeScript File Manager
  • A Basic TypeScript API Server

Node’s HTTP module is intentionally low-level. It gives direct access to request and response objects, which makes it a useful foundation before learning Express.js. (Node.js)

Prerequisite

Continue using the project created on Day 9.

day-10-node-typescript/
│
├── src/
│ └── index.ts
│
├── dist/
├── .env
├── package.json
└── tsconfig.json

Development command:

npm run dev

Type-check:

npm run typecheck

Production build:

npm run build
npm start

1. Node.js Core Modules

Node.js includes built-in modules that do not require installation from npm.

Today we’ll use:

node:fs
node:path
node:os
node:events
node:http
node:stream
node:buffer

Use the node: prefix to make it clear that a module is built into Node.js.

Example:

import path from "node:path";

2. File System Module

The node:fs module lets applications work with files and directories.

It supports:

  • Callback-based APIs
  • Synchronous APIs
  • Promise-based APIs
  • File streams

For most backend code, prefer the Promise API because synchronous file operations block further JavaScript execution while they run. (Node.js)

Import it:

import { readFile, writeFile } from "node:fs/promises";

3. Write a File

Create:

src/files/write-file.ts
import { writeFile } from "node:fs/promises";
async function createFile(): Promise<void> {
try {
await writeFile(
"message.txt",
"Hello from Node.js and TypeScript",
"utf8"
);
console.log("File created successfully");
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
}
}
await createFile();

Run:

npx tsx src/files/write-file.ts

This creates:

message.txt

4. Read a File

import { readFile } from "node:fs/promises";
async function readMessage(): Promise<void> {
try {
const content = await readFile(
"message.txt",
"utf8"
);
console.log(content);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(
`Unable to read file: ${error.message}`
);
}
}
}
await readMessage();

Without "utf8", readFile() returns a Buffer.

5. Append to a File

import { appendFile } from "node:fs/promises";
async function addLog(): Promise<void> {
const log = `Application started at ${new Date().toISOString()}\n`;
await appendFile(
"application.log",
log,
"utf8"
);
console.log("Log added");
}
await addLog();

This is useful for understanding basic logging, although production applications normally use dedicated logging libraries.

6. Delete a File

import { unlink } from "node:fs/promises";
async function removeFile(): Promise<void> {
try {
await unlink("message.txt");
console.log("File deleted");
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
}
}
await removeFile();

7. Check Whether a File Exists

import { access } from "node:fs/promises";
async function fileExists(
filePath: string
): Promise<boolean> {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
console.log(await fileExists("message.txt"));

8. Create a Directory

import { mkdir } from "node:fs/promises";
async function createDirectory(): Promise<void> {
await mkdir("data/users", {
recursive: true
});
console.log("Directory created");
}
await createDirectory();

The recursive option creates missing parent directories.

9. Read Directory Contents

import { readdir } from "node:fs/promises";
async function listFiles(): Promise<void> {
const files = await readdir("data");
console.log(files);
}
await listFiles();

To get information about whether each entry is a file or directory:

import { readdir } from "node:fs/promises";
const entries = await readdir("data", {
withFileTypes: true
});
for (const entry of entries) {
console.log({
name: entry.name,
isFile: entry.isFile(),
isDirectory: entry.isDirectory()
});
}

10. Rename or Move a File

import { rename } from "node:fs/promises";
await rename(
"old-name.txt",
"new-name.txt"
);
console.log("File renamed");

It can also move files:

await rename(
"report.txt",
"data/report.txt"
);

11. File Information with stat()

import { stat } from "node:fs/promises";
const information = await stat("application.log");console.log({
size: information.size,
created: information.birthtime,
modified: information.mtime,
isFile: information.isFile()
});

12. Path Module

Paths differ between operating systems.

Avoid manually joining paths like this:

const filePath = "data/" + "users/" + "user.json";

Use node:path:

import path from "node:path";
const filePath = path.join(
"data",
"users",
"user.json"
);
console.log(filePath);

13. Common Path Methods

path.join()

import path from "node:path";
const filePath = path.join(
process.cwd(),
"data",
"users.json"
);
console.log(filePath);

path.resolve()

Creates an absolute path:

const fullPath = path.resolve(
"data",
"users.json"
);
console.log(fullPath);

path.basename()

const name = path.basename(
"/projects/api/users.json"
);
console.log(name);

Output:

users.json

path.dirname()

const directory = path.dirname(
"/projects/api/users.json"
);
console.log(directory);

path.extname()

const extension = path.extname(
"users.json"
);
console.log(extension);

Output:

.json

path.parse()

const information = path.parse(
"/projects/api/users.json"
);
console.log(information);

Possible output:

{
root: "/",
dir: "/projects/api",
base: "users.json",
ext: ".json",
name: "users"
}

14. Working with the Current Module Directory

In ES modules, __dirname is not available automatically.

Use:

import path from "node:path";
import { fileURLToPath } from "node:url";
const currentFile = fileURLToPath(
import.meta.url
);
const currentDirectory = path.dirname(
currentFile
);
console.log(currentFile);
console.log(currentDirectory);

15. Operating System Module

Import:

import os from "node:os";

Get system information:

console.log({
platform: os.platform(),
architecture: os.arch(),
hostname: os.hostname(),
homeDirectory: os.homedir(),
temporaryDirectory: os.tmpdir(),
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
cpuCount: os.cpus().length
});

Convert bytes to gigabytes:

function bytesToGigabytes(
bytes: number
): string {
return (
bytes / 1024 / 1024 / 1024
).toFixed(2);
}
console.log(
`${bytesToGigabytes(os.totalmem())} GB`
);

16. Events Module

Node.js uses events heavily.

Import:

import { EventEmitter } from "node:events";

Create an event emitter:

const emitter = new EventEmitter();
emitter.on("message", (message: string) => {
console.log(message);
});
emitter.emit(
"message",
"Hello from EventEmitter"
);

17. Typed Event Payload

import { EventEmitter } from "node:events";
interface UserCreatedEvent {
id: number;
name: string;
email: string;
}
const userEvents = new EventEmitter();userEvents.on(
"userCreated",
(user: UserCreatedEvent) => {
console.log(
`User created: ${user.name}`
);
}
);
userEvents.emit("userCreated", {
id: 1,
name: "Ravi",
email: "ravi@example.com"
});

Note that the base EventEmitter API does not automatically enforce relationships between event names and their payload types. Later, a typed wrapper can provide stricter event safety.

18. once()

A listener registered with once() runs only one time.

const emitter = new EventEmitter();
emitter.once("connected", () => {
console.log("Connected for the first time");
});
emitter.emit("connected");
emitter.emit("connected");

Output:

Connected for the first time

19. Remove an Event Listener

const emitter = new EventEmitter();
function handleMessage(message: string): void {
console.log(message);
}
emitter.on("message", handleMessage);emitter.emit("message", "First message");emitter.off("message", handleMessage);emitter.emit("message", "Second message");

Only the first message is printed.

20. Buffers

A Buffer represents binary data.

const buffer = Buffer.from(
"Hello Node.js",
"utf8"
);
console.log(buffer);
console.log(buffer.toString("utf8"));

Buffers are commonly encountered when working with:

  • Files
  • Network data
  • Images
  • Uploads
  • Streams

21. Allocate a Buffer

const buffer = Buffer.alloc(4);
buffer[0] = 72;
buffer[1] = 105;
console.log(buffer.toString());

Output:

Hi

22. Streams

Streams process data in smaller pieces rather than loading everything into memory at once.

Node.js streams work with strings and binary values such as Buffer and typed-array objects. HTTP requests, HTTP responses and file streams all use streaming concepts. (Node.js)

The main stream categories are:

  • Readable
  • Writable
  • Duplex
  • Transform

23. Readable File Stream

import { createReadStream } from "node:fs";
const stream = createReadStream(
"large-file.txt",
{
encoding: "utf8",
highWaterMark: 64
}
);
stream.on("data", (chunk) => {
console.log("Chunk received:");
console.log(chunk);
});
stream.on("end", () => {
console.log("Finished reading");
});
stream.on("error", (error) => {
console.error(error.message);
});

24. Writable File Stream

import { createWriteStream } from "node:fs";
const stream = createWriteStream(
"output.txt",
{
encoding: "utf8"
}
);
stream.write("First line\n");
stream.write("Second line\n");
stream.end("Final line\n");
stream.on("finish", () => {
console.log("Writing completed");
});

25. Pipe Streams

import {
createReadStream,
createWriteStream
} from "node:fs";
const source = createReadStream(
"large-file.txt"
);
const destination = createWriteStream(
"large-file-copy.txt"
);
source.pipe(destination);

A safer modern approach is pipeline().

26. Stream Pipeline

import {
createReadStream,
createWriteStream
} from "node:fs";
import { pipeline } from "node:stream/promises";async function copyFile(): Promise<void> {
await pipeline(
createReadStream("large-file.txt"),
createWriteStream("copy.txt")
);
console.log("File copied");
}
try {
await copyFile();
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
}

The Promise-based stream utilities make it easier to handle completion and errors using async/await. (Node.js)

27. HTTP Module

Import:

import { createServer } from "node:http";

Node’s server callback receives:

  • IncomingMessage
  • ServerResponse

The request object contains information such as the URL, method, headers and incoming body stream. The response object is used to set status, headers and response content. (Node.js)

28. Create Your First HTTP Server

Create:

src/server.ts
import { createServer } from "node:http";
const port = 3000;const server = createServer(
(request, response) => {
response.statusCode = 200;
response.setHeader(
"Content-Type",
"text/plain; charset=utf-8"
);
response.end("Hello from Node.js");
}
);
server.listen(port, () => {
console.log(
`Server running at http://localhost:${port}`
);
});

Run:

npx tsx src/server.ts

Open:

http://localhost:3000

29. Request Information

const server = createServer(
(request, response) => {
console.log({
method: request.method,
url: request.url,
headers: request.headers
});
    response.end("Request received");
}
);

30. Send a JSON Response

import { createServer } from "node:http";
const server = createServer(
(_request, response) => {
const data = {
success: true,
message: "API is working"
};
response.writeHead(200, {
"Content-Type":
"application/json; charset=utf-8"
});
response.end(JSON.stringify(data));
}
);
server.listen(3000);

31. HTTP Methods

Common methods:

GET     Read data
POST Create data
PUT Replace data
PATCH Partially update data
DELETE Delete data

Read the method:

const method = request.method ?? "GET";

32. HTTP Status Codes

Common API status codes:

200 OK
201 Created
204 No Content
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
500 Internal Server Error

Example:

response.statusCode = 404;
response.end("Route not found");

33. Basic Routing

import { createServer } from "node:http";
const server = createServer(
(request, response) => {
const method = request.method;
const url = request.url;
if (method === "GET" && url === "/") {
response.writeHead(200, {
"Content-Type":
"application/json; charset=utf-8"
});
response.end(
JSON.stringify({
message: "Welcome to the API"
})
);
return;
}
if (
method === "GET" &&
url === "/health"
) {
response.writeHead(200, {
"Content-Type":
"application/json; charset=utf-8"
});
response.end(
JSON.stringify({
status: "healthy",
uptime: process.uptime()
})
);
return;
}
response.writeHead(404, {
"Content-Type":
"application/json; charset=utf-8"
});
response.end(
JSON.stringify({
message: "Route not found"
})
);
}
);
server.listen(3000, () => {
console.log(
"Server running at http://localhost:3000"
);
});

34. Parse a URL

The request URL may contain:

/users?page=2&limit=10

Use the URL class:

const url = new URL(
request.url ?? "/",
`http://${request.headers.host ?? "localhost"}`
);
console.log(url.pathname);
console.log(url.searchParams.get("page"));
console.log(url.searchParams.get("limit"));

35. Query Parameters

Example request:

GET /users?role=admin
const requestUrl = new URL(
request.url ?? "/",
"http://localhost"
);
const role = requestUrl.searchParams.get(
"role"
);
console.log(role);

36. Route Parameters

Node’s built-in HTTP module does not provide automatic route-parameter parsing.

Get Bhaskar Chowdary’s stories in your inbox

Join Medium for free to get updates from this writer.

For:

GET /users/10

You can parse it manually:

const parts = requestUrl.pathname
.split("/")
.filter(Boolean);
if (
request.method === "GET" &&
parts[0] === "users" &&
parts[1]
) {
const userId = Number(parts[1]);
if (!Number.isInteger(userId)) {
// Invalid ID
}
}

Express will make this easier on Day 11.

37. Reading a Request Body

An HTTP request body arrives as a stream.

import type {
IncomingMessage
} from "node:http";
async function readRequestBody(
request: IncomingMessage
): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of request) {
chunks.push(
Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk)
);
}
return Buffer.concat(chunks).toString(
"utf8"
);
}

38. Parse JSON Request Body

async function readJsonBody<T>(
request: IncomingMessage
): Promise<T> {
const body = await readRequestBody(
request
);
  if (!body) {
throw new Error(
"Request body is required"
);
}
return JSON.parse(body) as T;
}

Important:

JSON.parse(body) as T

does not perform runtime validation.

It only tells TypeScript to treat the result as T.

Later we’ll use Zod for actual validation.

39. Request Body Size Limit

Never accept unlimited request bodies.

async function readRequestBody(
request: IncomingMessage,
maximumBytes = 1_000_000
): Promise<string> {
const chunks: Buffer[] = [];
let totalBytes = 0;
  for await (const chunk of request) {
const buffer = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk);
totalBytes += buffer.length; if (totalBytes > maximumBytes) {
throw new Error(
"Request body is too large"
);
}
chunks.push(buffer);
}
return Buffer.concat(chunks).toString(
"utf8"
);
}

40. Response Helper

Create:

src/http/send-json.ts
import type {
ServerResponse
} from "node:http";
export function sendJson(
response: ServerResponse,
statusCode: number,
data: unknown
): void {
response.writeHead(statusCode, {
"Content-Type":
"application/json; charset=utf-8"
});
response.end(JSON.stringify(data));
}

Use it:

sendJson(response, 200, {
success: true
});

41. Basic In-Memory Users API

Create:

src/server.ts
import {
createServer
} from "node:http";
import type {
IncomingMessage,
ServerResponse
} from "node:http";
interface User {
id: number;
name: string;
email: string;
}
interface CreateUserInput {
name: string;
email: string;
}
const users: User[] = [
{
id: 1,
name: "Ravi",
email: "ravi@example.com"
}
];
let nextUserId = 2;function sendJson(
response: ServerResponse,
statusCode: number,
data: unknown
): void {
response.writeHead(statusCode, {
"Content-Type":
"application/json; charset=utf-8"
});
response.end(JSON.stringify(data));
}
async function readBody(
request: IncomingMessage
): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of request) {
chunks.push(
Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk)
);
}
return Buffer.concat(chunks).toString(
"utf8"
);
}
const server = createServer(
async (request, response) => {
try {
const method = request.method ?? "GET";
const requestUrl = new URL(
request.url ?? "/",
"http://localhost"
);
const pathname = requestUrl.pathname; if (
method === "GET" &&
pathname === "/"
) {
sendJson(response, 200, {
message: "Users API"
});
return;
}
if (
method === "GET" &&
pathname === "/users"
) {
sendJson(response, 200, {
success: true,
data: users
});
return;
}
if (
method === "POST" &&
pathname === "/users"
) {
const body = await readBody(request);
const input = JSON.parse(
body
) as CreateUserInput;
if (
typeof input.name !== "string" ||
typeof input.email !== "string"
) {
sendJson(response, 400, {
message:
"Name and email are required"
});
return;
}
const user: User = {
id: nextUserId++,
name: input.name,
email: input.email
};
users.push(user); sendJson(response, 201, {
success: true,
data: user
});
return;
}
sendJson(response, 404, {
message: "Route not found"
});
} catch (error: unknown) {
const message =
error instanceof SyntaxError
? "Invalid JSON body"
: "Internal server error";
sendJson(
response,
error instanceof SyntaxError
? 400
: 500,
{ message }
);
}
}
);
server.listen(3000, () => {
console.log(
"Server running at http://localhost:3000"
);
});

42. Test the API

Start the server:

npx tsx watch src/server.ts

Get all users

curl http://localhost:3000/users

Create a user

curl -X POST \
http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Arun","email":"arun@example.com"}'

You can also use:

  • Postman
  • Thunder Client
  • Insomnia

43. Graceful Shutdown

A server should handle termination signals.

function shutdown(signal: string): void {
console.log(
`${signal} received. Shutting down...`
);
  server.close((error) => {
if (error) {
console.error(error);
process.exit(1);
}
console.log("Server closed");
process.exit(0);
});
}
process.on("SIGINT", () => {
shutdown("SIGINT");
});
process.on("SIGTERM", () => {
shutdown("SIGTERM");
});

44. Server Error Handling

server.on("error", (error) => {
console.error(
`Server error: ${error.message}`
);
  process.exit(1);
});

Mini Project 1 — TypeScript File Manager

Create:

src/file-manager.ts
import {
access,
appendFile,
mkdir,
readFile,
unlink,
writeFile
} from "node:fs/promises";
import path from "node:path";class FileManager {
constructor(
private readonly baseDirectory: string
) {}
private resolvePath(
fileName: string
): string {
const safeName = path.basename(fileName);
return path.join(
this.baseDirectory,
safeName
);
}
async initialize(): Promise<void> {
await mkdir(this.baseDirectory, {
recursive: true
});
}
async exists(
fileName: string
): Promise<boolean> {
try {
await access(
this.resolvePath(fileName)
);
return true;
} catch {
return false;
}
}
async create(
fileName: string,
content: string
): Promise<void> {
await writeFile(
this.resolvePath(fileName),
content,
{
encoding: "utf8",
flag: "wx"
}
);
}
async read(
fileName: string
): Promise<string> {
return readFile(
this.resolvePath(fileName),
"utf8"
);
}
async append(
fileName: string,
content: string
): Promise<void> {
await appendFile(
this.resolvePath(fileName),
content,
"utf8"
);
}
async delete(
fileName: string
): Promise<void> {
await unlink(
this.resolvePath(fileName)
);
}
}

Use it:

const manager = new FileManager("storage");
try {
await manager.initialize();
if (!(await manager.exists("notes.txt"))) {
await manager.create(
"notes.txt",
"Day 10 Node.js Notes\n"
);
}
await manager.append(
"notes.txt",
"Learning the file system module\n"
);
const content = await manager.read(
"notes.txt"
);
console.log(content);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
}

Mini Project 2 — Basic TypeScript API Server

Build an API with:

GET    /health
GET /users
GET /users/:id
POST /users
DELETE /users/:id

Required concepts:

  • node:http
  • Typed models
  • Route parsing
  • Query parameters
  • JSON request bodies
  • Status codes
  • JSON responses
  • Central error handling
  • Graceful shutdown

Common Beginner Mistakes

Using synchronous file operations in request handlers

Avoid:

readFileSync("users.json");

inside every HTTP request.

Synchronous file operations block the event loop until they finish. Prefer Promise-based or stream-based APIs in server code. (Node.js)

Forgetting to end the response

Wrong:

response.write("Hello");

Correct:

response.end("Hello");

Sending multiple responses

Wrong:

if (request.url === "/") {
response.end("Home");
}
response.end("Not found");

Correct:

if (request.url === "/") {
response.end("Home");
return;
}
response.end("Not found");

Trusting parsed JSON

This is unsafe:

const user = JSON.parse(body) as User;

The request may not contain a valid user.

Check the value manually now. Later, use Zod.

Ignoring request-body limits

A client could send a very large payload.

Always define a size limit.

Building paths using user input directly

Avoid:

path.join("storage", userFileName);

without checking the filename.

A value such as:

../../secret.txt

could escape the expected directory.

At minimum, use:

path.basename(userFileName);

More complete protection will be discussed during the security phase.

Returning incorrect status codes

Use:

200 for successful reads
201 for successful creation
400 for invalid input
404 when a resource is missing
500 for unexpected server errors

Important Concepts Learned Today

✅ node:fs
✅ Promise-based file operations
✅ Files and directories
✅ node:path
✅ node:os
✅ EventEmitter
✅ Buffers
✅ Readable and writable streams
✅ Stream pipelines
✅ node:http
✅ HTTP request and response objects
✅ HTTP methods
✅ Routes
✅ Query parameters
✅ Route parameters
✅ Status codes
✅ JSON request bodies
✅ JSON responses
✅ Body-size limits
✅ Error handling
✅ Graceful shutdown
✅ Typed API models

Homework

Create:

  1. File creation program
  2. File reading and updating program
  3. Directory listing program
  4. System-information program using node:os
  5. Custom event system using EventEmitter
  6. Large-file copy using streams
  7. Health-check HTTP route
  8. Users API with GET and POST
  9. Request-body size limit
  10. Graceful server shutdown

Practice Challenge

Build a typed tasks API:

GET    /tasks
GET /tasks/:id
POST /tasks
PATCH /tasks/:id
DELETE /tasks/:id

Task structure:

interface Task {
id: number;
title: string;
completed: boolean;
}

Requirements:

  • Use only Node.js core modules
  • Store tasks in memory
  • Return JSON responses
  • Validate incoming data
  • Use correct status codes
  • Handle invalid JSON
  • Return 404 for missing tasks

Day 11

Express.js Basics with TypeScript

We’ll learn:

  • Why Express is useful
  • Express + TypeScript setup
  • Application and router objects
  • Request and response types
  • Route handlers
  • Middleware
  • Route parameters
  • Query parameters
  • JSON body parsing
  • Modular routes
  • Typed controllers
  • Error handling introduction

You’ll rebuild today’s Node.js HTTP server using Express with much less code.

--

--

Bhaskar Chowdary
Bhaskar Chowdary

I believe in There is No Problem Without Solution