Your First Express App
The fastest way to understand Express is to build the smallest possible working server: create an app, define one route, and start it listening on a port. In a dozen lines you have a real HTTP server you can hit from a browser or curl. This page walks through the canonical app.js, explains every line, and shows how to run it with Node — the foundation that every larger Express application grows from.
The complete Hello World app
Create a file named app.js in an empty project folder. (If you have not installed Express yet, see Installation and setup first — you need express in your node_modules.)
const express = require("express");
const app = express();
const PORT = 3000;
app.get("/", (req, res) => {
res.send("Hello, World!");
});
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
That is a fully functional web server. Run it and visit http://localhost:3000 in your browser, and you will see Hello, World! rendered as plain text.
Running the app
Start the server from your terminal with the Node runtime:
node app.js
Output:
Server running at http://localhost:3000
The process stays alive and keeps listening — it will not return to your shell prompt until you stop it with Ctrl+C. While it runs, send it a request from another terminal:
curl http://localhost:3000
Output:
Hello, World!
Tip: Running
node app.jsdoes not reload when you edit the file. During development, use a watcher so changes restart the server automatically —node --watch app.js(Node 18+) or a tool likenodemon.
Line by line
Each line in app.js plays a specific role. Understanding them now makes every later Express concept easier.
Importing Express
const express = require("express");
This loads the Express module. express is a function — calling it creates a new application instance. (If your project uses ES modules, with "type": "module" in package.json, write import express from "express"; instead.)
Creating the application
const app = express();
app is your application object. It holds your routes and middleware and exposes the methods you will use throughout the app: app.get(), app.post(), app.use(), app.listen(), and more. Almost everything you do in Express happens through this object.
Defining a route
app.get("/", (req, res) => {
res.send("Hello, World!");
});
This registers a route handler. It tells Express: when a GET request arrives for the path /, run this function. The handler receives two key arguments:
| Argument | Stands for | What it is |
|---|---|---|
req | request | The incoming request — URL, headers, query string, body, params |
res | response | The response object you use to send data back to the client |
Here res.send() sends the response. Express inspects what you pass it and sets sensible headers automatically: a string becomes text/html, while an object or array is serialized to JSON. The function returns nothing — sending the response is the result.
Starting the server
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
app.listen() binds the app to a TCP port and begins accepting connections. The optional callback fires once the server is ready, which is the right place to log a startup message. Until you call listen, your routes are defined but nothing is actually serving traffic.
Returning JSON instead of text
Most real Express apps serve JSON APIs rather than plain text. Swapping res.send() for res.json() is all it takes — Express sets the Content-Type: application/json header and serializes the value for you.
app.get("/api/health", (req, res) => {
res.json({ status: "ok", uptime: process.uptime() });
});
A GET /api/health request now returns:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"status":"ok","uptime":12.84}
You can also use an async handler when your route needs to await a database call or another service:
app.get("/api/time", async (req, res) => {
const now = await Promise.resolve(new Date().toISOString());
res.json({ now });
});
Note: In Express 5, if an
asynchandler rejects, the error is forwarded to your error-handling middleware automatically. In Express 4 you musttry/catchand callnext(err)yourself. See Express 4 vs 5 for the full list of differences.
Setting the port from the environment
Hardcoding the port is fine for a first app, but production environments (and platforms like Heroku, Render, or Docker) often inject the port through an environment variable. Read it with a sensible fallback:
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Now the same code runs locally on 3000 and adapts to whatever port the host assigns.
Best Practices
- Keep your entry file (
app.jsorserver.js) small — its job is to create the app, wire up middleware, and calllisten. - Read the port from
process.env.PORTwith a local fallback so the app is portable across environments. - Use
res.json()for API responses andres.send()for simple text or HTML — let Express set the headers for you. - Log a clear startup message in the
listencallback so you can confirm the server is ready and on which port. - Use
node --watchornodemonin development so you do not have to restart the server by hand after every edit. - As soon as you add a second route, start thinking about structure — split routes into routers and move logic out of handlers.