What Express Is & Your First Server
You know JavaScript — functions, callbacks, async/await,
modules. You want to put something on the web: an API a phone app or a frontend can talk to. You
could build it on Node's built-in HTTP server by hand (that guide
shows exactly how, and it's worth seeing once). But you'll quickly find yourself rewriting the same
plumbing — matching URLs, parsing bodies, sending JSON — for every project. That plumbing, packaged
into something small and well-worn, is Express.
Here's the one idea to hold before any code. Express is deliberately tiny — a thin layer wrapped
around the node:http module you'd otherwise use raw. It gives you two things: a clean way to
route requests, and a system called middleware for everything else. Body parsing, auth,
logging, validation? Those aren't built in — you add them as middleware when you need them. That
minimalism is Express's whole personality, and it's why a decade-plus of Node jobs, tutorials, and
production servers run on it. It's the framework you're most likely to meet.
📝 Express — the dominant minimalist web framework for Node.js. Small core (routing + middleware), everything else bolted on. It doesn't replace Node's HTTP server; it sits on top of it and makes it pleasant to use. If you've read What a Framework Even Is, Express is the textbook case: a small, unopinionated one that stays out of your way.
The mental model: a pipeline of functions
Before the code, the single picture that explains everything Express does.
💡 An Express app is a pipeline of (req, res, next) functions, and a route is one of them bound
to a method and a path. A request comes in, flows through an ordered chain of functions, and each
one can read the request, change it, send a response, or call next() to hand off to the next
function in line. That's it. Routing, body parsing, auth, error handling — they're all the same
shape ((req, res, next)) in different costumes. This chain is the heart of Express, and Phase 3 is
devoted to it. For now, hold the picture: request → functions → response.
A route is the simplest member of that family: a function bound to one HTTP method (like GET) and
one path (like /). Let's build one.
Your first server
One install gets you the framework:
What just happened: npm downloaded Express and its dependencies into node_modules, and recorded
it in your package.json. You now have the whole framework available to require (or import). This
assumes you've run npm init -y first to create a package.json — if you haven't, do that, then
install.
Now the smallest server that does something. Create a file called index.js:
const express = ;
const app = ;
app.;
app.;
What just happened: four moves, and they're the four you'll use forever.
const app = express();calls the Express function to create your application object — the thing that holds your routes and middleware and, eventually, runs the show.app.get('/', handler)registers a route: "when aGETrequest arrives for the path/, run this handler." You never call the handler yourself — Express calls it when a matching request comes in. (That's the framework's "don't call us, we'll call you" relationship.)- The handler receives
(req, res)— the request (what the client sent) and the response (what you send back).res.send('Hello from Express')writes that string as the response body and finishes the request. Express sets sensible headers (likeContent-Type: text/html) for you. app.listen(3000, ...)starts the underlying HTTP server and binds it to port 3000. The callback fires once it's up, so you know where to look.
Run it with plain Node — Express is just a library, there's no special CLI:
$ node index.js
listening on http://localhost:3000
What just happened: Node executed your file, Express handed its request handler to node:http, and
the server is now listening. It will keep running, waiting for requests, until you stop it with
Ctrl+C. Open a second terminal and hit it:
$ curl localhost:3000
Hello from Express
What just happened: curl sent a GET / request. Express matched it against the route you
registered, called your handler, and sent back the string. You have a working web server in seven
lines of real code.
⚠️ The handler must end the request — exactly once. Every request needs a response. If your
handler never calls res.send (or res.json, or res.end), the client just hangs until it times
out. And if you call res.send twice in one handler, Node throws ERR_HTTP_HEADERS_SENT — the
response was already sent, you can't send it again. One request, one response. Keep that in your
bones and a whole category of confusing bugs never happens.
📝 CommonJS vs ESM. The example uses
require(CommonJS), the long-standing Node default and what most Express tutorials show. If yourpackage.jsonhas"type": "module", use the modern ESM form instead:import express from 'express';. The rest is identical. We'll stick withrequirethrough this guide so examples are copy-paste runnable on any setup.
Sending JSON, not just text
A string is fine for "hello world," but real backends usually speak JSON. Express has a dedicated method for it. Add a second route:
app.;
app.;
What just happened: res.json(obj) serializes a JavaScript object to a JSON string and sets
Content-Type: application/json automatically — so the client knows it's getting JSON, not plain
text. That header is the real difference between res.send and res.json. You could call
res.send(JSON.stringify(obj)), but then you'd be setting the content type yourself; res.json is
the honest, complete way to return JSON. (res.send is the generalist — it'll send strings, Buffers,
and even objects, guessing the type — but reach for res.json whenever you mean JSON.)
Restart the server (Ctrl+C, then node index.js — Node doesn't auto-reload on file changes; we'll
fix that in a later phase) and check the new route:
$ curl localhost:3000/health
{"status":"ok","uptime":4.21}
What just happened: the request for /health matched the second route, Express called its handler,
serialized the object, and sent it back with the JSON content type. Two paths, two handlers, same
pattern — add a hundred routes and it's this idea a hundred times.
The running example: a tasks API
One more thing before we go deeper. Across this guide we'll grow one real service so each concept lands on something concrete instead of a toy. Meet the tasks API — a small to-do backend where each task is an object shaped like this:
const tasks = ;
app.;
What just happened: tasks is just an in-memory array of objects, each with id, title, and
done. The GET /tasks route returns the whole list as JSON — the first endpoint of an API we'll
turn into full create/read/update/delete (CRUD) over the next phases. In-memory means the data resets
every time the server restarts; that's fine for learning, and we'll talk about real storage later.
Hitting it:
$ curl localhost:3000/tasks
[{"id":1,"title":"Learn Express routing","done":false},{"id":2,"title":"Understand middleware","done":false}]
What just happened: the route returned the array, res.json serialized it, and curl printed the
JSON. You've now seen the entire shape of an Express endpoint — method, path, handler, response — and
you have a real API with one route. Next we make routes carry data (a task's id in the URL, query
strings, separate router files), which is Phase 2: Routing.
Recap
- Express is the minimalist Node.js web framework — a thin layer over the built-in
node:httpmodule. Small core (routing + middleware), everything else added on. Install withnpm install express. - The one big idea: an Express app is a pipeline of
(req, res, next)functions, and a route is one bound to a method + path. Routing, parsing, auth, and errors are all that one shape. (Full treatment in Phase 3.) - A first server is four moves:
const app = express()creates the app;app.get(path, handler)registers a route; the handler gets(req, res);app.listen(port)starts it. Run it with plainnode index.js— Express has no special CLI. res.sendvsres.json:res.sendis the generalist;res.json(obj)serializes an object and setsContent-Type: application/json. Useres.jsonwhenever you mean JSON.- ⚠️ Every request needs exactly one response. Forget to respond and the client hangs; respond
twice and Node throws
ERR_HTTP_HEADERS_SENT. - Our running example is a tasks API (
{ id, title, done }), starting from a singleGET /tasksroute and growing into full CRUD across the guide.
Quick check
Three questions on what has to stick — what Express is, how a first server is wired, and how to return JSON:
[
{
"q": "What is Express, in one line?",
"choices": [
"A minimalist web framework that's a thin layer over Node's built-in http module, giving you routing and a middleware system",
"A standalone web server written in C that replaces Node entirely",
"A database for storing JSON documents in Node apps",
"A frontend UI library for building components in the browser"
],
"answer": 0,
"explain": "Express is the dominant minimalist Node.js web framework. It doesn't replace Node's HTTP server — it sits on top of node:http and adds clean routing plus a middleware system, leaving everything else to middleware you add."
},
{
"q": "In `app.get('/', (req, res) => { ... })`, what does this line do and who calls the handler?",
"choices": [
"It registers a route for GET requests to '/', and Express calls the handler when a matching request arrives",
"It immediately runs the handler once and caches the result",
"It sends a GET request to '/' and returns the response",
"It defines a route but you must call the handler yourself in app.listen"
],
"answer": 0,
"explain": "app.get(path, handler) registers a route. You never call the handler yourself — Express matches incoming requests by method and path and calls the handler for you. That's the framework's 'don't call us, we'll call you' relationship."
},
{
"q": "What does `res.json(obj)` do that `res.send(JSON.stringify(obj))` does not?",
"choices": [
"It serializes the object to JSON AND automatically sets the Content-Type header to application/json",
"It saves the object to a database before responding",
"It validates that the object matches a schema",
"Nothing — they are exactly identical in every way"
],
"answer": 0,
"explain": "Both produce a JSON string, but res.json also sets Content-Type: application/json automatically, so the client knows it's receiving JSON. With res.send(JSON.stringify(obj)) you'd have to set that header yourself. Reach for res.json whenever you mean JSON."
}
]
Guide overview · Phase 2: Routing →
Check your understanding
1. What is Express, in one line?
2. In `app.get('/', (req, res) => { ... })`, what does this line do and who calls the handler?
3. What does `res.json(obj)` do that `res.send(JSON.stringify(obj))` does not?