In a world with , chapter 4.1.

THE PATH · SESSION 17 OF 66 · UNIT 4 · ACT I

The Runtime Beyond the Browser

What Node.js is, and what Express is

45 min read3 diagrams2 side quests1 runnable2,670 words3 code blocks

At a glance

PACE
Session 17 of 66 · suggested week 6. Continue when you are ready.
UNIT
Unit 4 · I Built a Party of Four: Mongo, Express, React, Node
ACT
Act I · Foundation
BEFORE
3.6 The Arena
AFTER
4.2 The Waiting Room
ARTIFACT
A one-file Express app that answers GET / with text, and a note of what Node is doing underneath
READ
45 min · 3 diagrams · 3 code blocks

The hook#

The browser has been the whole world so far. HTML, CSS, a game that runs because Chrome runs it. Close the tab and the program is gone. Node is the other place JavaScript runs: not in a tab, on the machine, as a process you start from the terminal. Express is not a second language and not a second runtime. It is a small library, installed with npm, that sits on Node's built-in HTTP server and lets you attach functions to methods and paths.

By the end of this session you can say what Node is, what it is not, and what Express adds. You will have a one-file server that answers GET /. The rest of Unit 4 is that process growing a router, a database, pages, a JSON API, passwords, and finally a React screen. None of that fits in a process that only exists inside a browser tab.

The page was a painting of a door. The server is a door that stays open after you look away.

The idea#

Node's own introduction is the sentence to keep: Node.js is a JavaScript runtime. The language is still JavaScript. The runtime is the program that executes it. In the browser that program is the browser, and the famous engine inside Chrome is V8. Node took V8 out of the browser and gave it a standard library aimed at servers and command-line tools: files, processes, sockets, and a module system. There is no window, no document, and no DOM. A script that starts with document.querySelector is a browser script. A script that starts with require('node:http') or import http from 'node:http' is a Node script. They can share functions you wrote. They cannot share the host.

That split is why "it works on my page" is not evidence about a server, and why a tutorial that pastes browser code into a file named server.js fails in a way that looks like JavaScript is broken. JavaScript is fine. You asked a runtime for an object it does not have.

Node runs your JavaScript on a single thread. One call stack. When you write a loop that burns CPU, nothing else in that process moves, including other people's requests. The reason a Node server can still hold many connections is the same reason the browser stays responsive while an image downloads: the slow work is not done on the JavaScript thread. Reading a file, talking to a database, and waiting on a socket are handed to the operating system. When they finish, a callback is queued. The event loop pulls from that queue when the stack is empty. You already lived this in the browser, with clicks and timers as the events. On the server the events are I/O. The design is a gift for waiting and a trap for heavy computation. A checksum of a giant file, done in a tight loop in the request handler, stalls every other client. The fix is not "Node is fast." The fix is to keep the thread free, which is a later conversation. Today, know which work belongs on the thread and which work is a wait.

You can serve HTTP without Express. Node's node:http module creates a server. You pass a function, and that function is called with a request and a response for every connection. You write the status and the body yourself, and you end the response yourself. Express is a layer on top of that function. The Express install guide is three commands and a dependency. The hello-world guide is the whole idea in one file: create an application, register a route with a method and a path, and listen on a port. Express does not open a port by existing. listen does. Until listen succeeds, you have a description of a server, not a server. After it succeeds, the operating system delivers connections for that port to your process. Port 3000 is a habit, not a law. If another process already has it, Node raises EADDRINUSE and exits. Two servers do not share a port politely.

npm is how the dependency arrives. npm init writes package.json, the manifest of your project. npm install express adds Express to that manifest and extracts the files into node_modules. The lockfile records the exact tree you installed. Commit the manifest and the lockfile. Do not commit node_modules. The next machine installs from the lockfile. That is the difference between "it worked here" and "it can be built again."

A route handler is a function of (req, res). req is the incoming request: method, path, headers, and later a body. res is how you answer. res.send writes a body and finishes the response. res.json does the same for a value it serializes, and sets the content type. If the function returns without sending, the connection stays open. The browser spinner is not a mystery. You never answered. Express will not invent a reply for you.

This unit uses Express 5. The behavior you will lean on next session is in the error-handling guide: an async route that throws, or returns a promise that rejects, is forwarded to the error handler. You do not wrap every handler in try/catch just to call next(err). Older posts written for Express 4 will tell you that you must. Read the page that matches the version in your package.json, not the first result in a search.

What you should be able to draw on a napkin: a browser and Node share a language and not a platform. Node is one thread plus an event loop fed by I/O. Express is a router and a middleware stack on node:http. A server is a process that has successfully called listen. The response is your job.

Session 4.1: The working model. Node: A JavaScript runtime on the machine, with its own module system and an event loop. http: The built-in module that opens a port and hands you a request and a response. Express: Functions you mount on methods and paths. It calls them when a request matches.
Fig 1 · The working model: Node → http → Express

The three objects, and nothing else#

Keep the vocabulary small enough to say out loud.

Node is the runtime. You start it with the node command. node -v is the version you are actually running. Write it down. A later deploy that uses a different major version is a different program, even when the files match.

http is the built-in module. It speaks the protocol: a request line, headers, a body, and a response in the same shape. You rarely call it once Express is installed, but when a bug is underneath Express, this is the layer that owns the socket.

Express is the package. express() returns an application. app.get, app.post, and the other method helpers register a match. app.listen is what binds the port. The application object is a facade over the harder module. The facade is the point. You do not lose the right to drop down. You just should not start there.

A request, in the order it actually happens#

Someone, later it will be a browser and today it is curl, opens a TCP connection to your machine on the port you bound. Node accepts it. The HTTP parser inside node:http turns bytes into a request object: a method, a URL, headers, and eventually a body if there is one. Express walks the middleware and routes you registered, in the order you registered them, and calls the first match. Your function runs on the same thread as every other request's JavaScript. If you send a response, Express finishes the HTTP message and the socket can be reused. If you throw, Express 5's default path is the error handler, which you have not customized yet, so the client gets a 500 and, outside production, a stack trace. If you do neither, the socket sits there until the client gives up.

None of those steps is a framework concept you need a diagram to survive. They are the reason the log line, the curl, and the hung request are three different facts. "The server is running" only means listen returned. "The route works" means a client received the body you intended, with the status you intended. Get used to checking both. A green log line is not a response.

What a module is, here#

Node loads code from files. CommonJS is require and module.exports. ECMAScript modules are import and export, which you opt into with "type": "module" in package.json or by naming the file .mjs. The Express docs show both. Pick one for this unit and stay with it. Mixing them in the first file is how a working example becomes a syntax error that has nothing to do with servers. This lesson's samples use CommonJS, because a fresh npm init still speaks it unless you say otherwise.

require('express') looks in node_modules. require('node:http') is the built-in, and the node: prefix is how you say so on purpose. A file of your own is a relative path, require('./quest'). There is no magical global where your functions appear. If you did not import it, it is not there.

The demonstration#

One folder, one manifest, one file. The folder is ~/projects/guild-door, and it is the project you keep through the rest of this half of the unit: every session up to the seals in 4-9 edits this same folder. From a terminal:

SH
cd ~/projectsmkdir guild-door && cd guild-doornpm init -ynpm install express

package.json now lists express under dependencies. node_modules/express exists. The lockfile exists. Then server.js:

JS
const express = require('express'); const app = express();const port = process.env.PORT || 3000; app.get('/', (req, res) => {  res.send('The door is open.');}); app.listen(port, () => {  console.log('listening on ' + port);});

process.env.PORT is how a host will tell you the port later. Locally it falls through to 3000. Start it with node server.js. The log line prints only if listen succeeded. In another terminal, curl http://localhost:3000/ returns the sentence. curl is already installed on macOS, on WSL, and on Linux; curl --version proves it. It is the tool this path uses to knock on a server from outside the process, for the rest of the unit. curl -i shows the status line, 200, and the headers Express set. Stop the process with Ctrl-C. curl then fails to connect. The program did not "keep the site up." The process was the site.

Change the handler to return without res.send, restart, and curl again. The request hangs. That is the lesson, not a broken install. Put the send back.

Start a second node server.js without stopping the first. You get EADDRINUSE. The port is taken. This is the error you will see again on a deploy, usually because a previous process is still alive, not because Express is cursed.

JScheckpoint-4-1.js
function check(condition, message) {  if (!condition) throw new Error(message);  console.log('PASS:', message);} function describeHost(name) {  if (name === 'browser') return { document: true, httpModule: false, listen: false };  if (name === 'node') return { document: false, httpModule: true, listen: true };  throw new Error('unknown host');} const browser = describeHost('browser');const node = describeHost('node');check(browser.document === true && browser.httpModule === false, 'the browser has a document and no http module');check(node.document === false && node.httpModule === true && node.listen === true, 'node has http and listen, and no document'); function answer(sent) {  return sent ? { status: 200, hanging: false } : { status: null, hanging: true };}check(answer(true).hanging === false, 'sending a body finishes the response');check(answer(false).hanging === true, 'returning without a body leaves the request hanging');console.log('ok: checkpoint-4-1');

The checkpoint is a model of the distinction, not a running server. The Run button cannot bind a port inside this page. The server you start in your own terminal is the demonstration. The model only checks that you can say which host owns which object, and what happens when a handler forgets to answer.

Where it breaks#

Your turn#

The quest#

Session 4.1: What done looks like. Version: node -v prints the runtime you will match in later deploys. Listen: The process prints the port only after listen succeeds. Answer: curl localhost:3000 returns the body you passed to res.send.
Fig 3 · What done looks like: Version · Listen · Answer

Write a one-page note, RUNTIME.md, in the same folder, in your own words. Six sentences, no more, covering:

  • what Node is, and what it is not
  • what Express adds that node:http does not hand you for free
  • what listen does, and what EADDRINUSE means
  • why a handler that returns without res.send hangs
  • the version of Node you are on, and the version of Express in your lockfile
  • where you would look if an old tutorial disagrees with your error-handling behavior

Then commit the manifest, the lockfile, server.js, .gitignore, and the note. Do not commit node_modules. Create an empty GitHub repository named guild-door and push, the way you did in 1-4; do not let GitHub add a README, since the folder already has files. This repository is the door. Every session through 4-9 builds in ~/projects/guild-door.

Side quests#

Express is a small door in front of a larger machine. Two library pages name that shape.

Side quests from this session

  • FacadeExpress is a facade: a small, pleasant surface over Node's http module. This sheet names that shape, a simple interface in front of a messier subsystem.
    Design Patterns
  • ProxyThe Proxy pattern is the other half of the word you will meet again in Unit 6. Read it now so 'the server' and 'the thing in front of the server' are already different objects in your head.
    Design Patterns

Before next session#

Next session the door stays exactly as it is, and the knock changes. Instead of curl, a second file beside server.js, named client.js, asks your server for / from JavaScript with fetch, and you watch that one request end three ways: the server answers 200, the server answers 404, and the server is stopped so nothing answers at all. The thing your program holds while it waits is a promise, and await is how you wait on it. You will meet try/catch around an await, and the difference between a response that said no and no response. Read the first paragraph of the MDN page on Promise before you sit down. You are not memorizing it. You are arriving knowing that a promise is pending until it settles, and that fetch hands you one.

One more distinction to carry in, because every later lesson leans on it. A module you import is not running a server. Importing Express creates nothing on the network. Calling express() creates an application object in memory. Only listen asks the operating system for a port. When a tutorial says "run the file" and you instead paste it into a browser console, you have skipped the only step that makes it a server. When a tutorial says "it is running" and you have not sent a request from outside the process, you have a log line and a guess. Curl, or a browser pointed at http://localhost:3000, is the other half of the demonstration. Next session that other half is a file you wrote.

Stop the server with Ctrl-C before you close the terminal. Nothing needs to stay running between sessions. The next one starts in this same folder with server.js unchanged, by adding client.js beside it; the router, the logger, and the error handler come the session after that.

See also