Until now, every line of your JavaScript has lived inside a browser tab. Phase 4 breaks it out: the same language, running directly on your computer, powering a server β a program that answers requests. Your Library app is about to get a backend.
tsc, Node was the thing actually running the compiler.
Node is NOT a new language. It's a different place for the language you already know to run β with different powers, because the world around the code is different:
| Browser JS (what you've done) | Node JS (Phase 4) | |
|---|---|---|
| Started by | a <script> tag in a page | node file.js in the terminal |
| Can see | document, DOM, buttons, alert, prompt, localStorage | files on disk, the network, environment β no DOM at all |
| Output goes to | the page + DevTools console | the terminal |
| Who runs it | every visitor's machine | YOUR machine (or a rented server) |
| Still the same | variables, functions, arrays, objects, map/filter/find, arrows, JSON.stringify/parse, console.log, Dates β everything from Phase 2/3 travels with you | |
document.getElementById("bookRows") in a Node file and run it. What happens?
β ReferenceError: document is not defined β the same error family as your Task 2.1
code is not defined. There is no page, so there is no document. Node isn't
"worse" β it simply lives where pages don't exist. Mirror image: the browser has no fs
(file system), because letting web pages read your disk would be a disaster.
tsc?" β and the answer
was the terminal, tsc is a program on your computer. That WAS Node: tsc is
JavaScript running in Node. Phase 4 just makes you the author of such programs instead of a user.
# 1. run a file (the normal way)
node hello.js
# 2. REPL β an interactive console, like DevTools but in the terminal
node
> 2 + 2
4
> [1,2,3].map(n => n * 5)
[ 5, 10, 15 ]
> .exit
node and run it. Same rule as always: observe,
don't predict-and-hope.
# inside your project folder β creates package.json (asks a few questions)
npm init
# install a package LOCALLY into this project
npm install fastify
After that, three things exist β know what each is:
| Thing | What it is | Your rule |
|---|---|---|
package.json | your project's identity card: name, scripts, and the dependencies list ("fastify: ^5β¦") | you edit this (carefully) |
node_modules/ | the actual downloaded code of every dependency (thousands of files) | NEVER edit β regenerable, like app.js. Delete it any time; npm install rebuilds it from package.json |
package-lock.json | the exact resolved versions, so installs are identical everywhere | don't edit, don't delete β npm maintains it |
npm install -g typescript put tsc in AppData\Roaming\npm β
machine-wide, a tool you run by name. npm install fastify (no -g)
puts code in THIS project's node_modules β a dependency your code imports.
Rule of thumb: commands you type β global; code you import β local.
And like tsc --init, plain npm install cares which folder you're standing
in β always run it in the project root (where package.json lives).
So far your whole app is ONE file. Real projects split code into modules β files
that explicitly say what they share (export) and what they borrow (import):
// books.ts β shares things
export interface Book { id: number; title: string; }
export function findBook(books: Book[], id: number) {
return books.find(b => b.id === id);
}
// server.ts β borrows them
import { Book, findBook } from "./books.js";
exports is not defined crash (2026-08-27)
Now the full story. There are TWO module systems: CommonJS (2009-era Node:
require() / module.exports β you'll see it in older code everywhere) and
ESM (the official standard: import / export). When your
tsconfig said module: "nodenext", tsc translated your file into CommonJS for
Node β emitting plumbing like exports.foo = .... The browser has no
exports variable β ReferenceError, then the cascade. Same code, wrong
world β exactly the document-in-Node crash, mirrored. In Phase 4 the code is FOR Node,
so those settings finally become correct instead of a trap.
"type": "module" in package.json β .js files are ESM (import/export).
Without it β CommonJS (require). We'll set "type": "module" and use modern
import/export everywhere. One wrinkle to expect: in ESM, import paths need the extension β
from "./books.js" (yes, .js even in a .ts file β you import
what the COMPILED file will be called).
Strip the mystique: a server is a program that waits for requests and sends back responses. That's the whole job. It runs forever in a loop: listen β request arrives β build an answer β send it β listen again.
localhost (= the IP 127.0.0.1) means "this same machine" β your browser
talking to your own Node process, no internet involved. A port is a numbered door
on a machine, because one computer runs many networked programs at once β they can't all answer the
same knock. Web defaults: 80 (http), 443 (https); dev servers pick free high numbers like
3000. So http://localhost:3000/books reads as: "this machine, door
3000, path /books".
Requests follow a standard: HTTP. A request names a method (what kind of action) and a path (what thing). You already built all four actions in the Library β HTTP just gives them official names:
| Method | Meaning | Your Library equivalent |
|---|---|---|
GET | read, change nothing | renderBooks, searchBooks, showLowStock |
POST | create something new | addBook, borrowBook (creates a Loan) |
PUT / PATCH | update something existing | returnBook (flips returned, copies+1) |
DELETE | remove | deleteBook |
The response carries a status code β a number stating how it went:
| Code | Means | Your app's version of it |
|---|---|---|
200 OK | worked, here's your data | a successful render |
201 Created | POST succeeded, thing now exists | addBook pushing a new book |
400 Bad Request | client sent nonsense | your addBook validation (isNaN β alert) |
404 Not Found | no such thing | your if (!book) return; guard |
500 Server Error | server itself crashed | an unhandled bug in your code |
JSON.stringify to send,
JSON.parse to receive β the EXACT pair you've used since localStorage. Same skill,
longer cable. (And the same trap travels too: Dates cross the wire as ISO strings β your capstone
rule "store facts, wrap in new Date() for math" applies unchanged.)
// server.ts β a complete working server
import Fastify from "fastify";
const app = Fastify();
app.get("/hello", () => {
return { message: "Library server is alive" };
});
await app.listen({ port: 3000 });
console.log("Listening on http://localhost:3000");
Run it, then visit http://localhost:3000/hello in the browser β JSON appears. Read the
shape: app.get(path, handler) β "when a GET request hits this path, run this function
and send back what it returns." Return an object β Fastify auto-converts to JSON
(JSON.stringify'd for you).
node --watch server.js and it restarts itself on every save β the server-world's
version of your editβtscβrefresh chain, with the refresh automated).
Each app.get/app.post/... line is a route. The Library's
backend will grow into this list β read it and notice it's exactly your function list, renamed:
GET /books β all books (renderBooks' data source)
GET /books/:id β one book (your find-by-id)
POST /books β add a book (addBook)
DELETE /books/:id β remove a book (deleteBook)
POST /loans β borrow (borrowBook)
PATCH /loans/:id β return a loan (returnBook)
Two new pieces inside handlers:
// :id is a URL PARAMETER β /books/3 β request.params.id is "3"
app.get("/books/:id", (request) => {
const id = Number((request.params as { id: string }).id); // params arrive as STRINGS
const book = books.find(b => b.id === id);
// ...guard β 404, else return book
});
// POST bodies: the JSON the client sent, already parsed for you
app.post("/books", (request) => {
const newBook = request.body as Book;
// ...validate β 400, else push + return 201
});
/books/3 delivers "3", a STRING β the form-input trap from Phase 2, back
in a new coat. Compare b.id === "3" with === and it's silently false for
every book (number vs string never strictly equal). Same cure as always: Number() at
the border, immediately. Everything that crosses a boundary β form, localStorage, URL, HTTP β
arrives as a string.
The plan, in order β each step is a working app:
| Step | What changes | What it teaches |
|---|---|---|
| 1. Hello server | Fastify answers /hello | setup, routes, the waiting loop |
| 2. GET /books | server owns the books array (in memory) | routes returning real data |
| 3. Frontend fetches | index.html calls fetch("http://localhost:3000/books") instead of reading localStorage | fetch, async/await β the missing JS piece |
| 4. Full CRUD routes | add/delete/borrow/return move server-side | POST bodies, params, status codes |
| 5. Save to a JSON file | server writes books.json/loans.json to disk (fs) | persistence without a browser β localStorage's server cousin |
| 6. β Phase 5 | the JSON file becomes a real database | SQL β and your bookId foreign key becomes literal |
Same two-step you already live by, new runner: edit .ts β tsc β node
server.js (instead of refresh). And here's the satisfying part: tsconfig's
module: "nodenext" β the setting that CRASHED your browser app β is the correct
setting now, because the output finally runs where it was designed to run: Node. Types get more
valuable here too β no DevTools on a server, so the more bugs die at compile time, the better.
| Idea | One line to remember |
|---|---|
| Node | the browser's JS engine, freed β same language, no DOM, plus files & network |
| npm local vs -g | commands you type β global; code you import β local |
| node_modules | regenerable, never edited β the app.js rule, scaled up |
| import/export | each file a private room; exports are what's passed through the door |
| Server | a program that waits: request in β response out, forever |
| HTTP methods | GET read Β· POST create Β· PATCH update Β· DELETE remove β your CRUD, officially named |
| Boundaries | whatever crosses one (form, URL, HTTP, storage) arrives as a string β convert at the border |
| The split | data lives on the server, presentation in the browser, JSON between them |
β Prev: TypeScript Β· Back to contents Β· Next: SQL & Prisma β