Skip to content
Blog

Node.js vs Express: What’s the Difference and Do You Need Both?

Node.js vs Express: What's the Difference and Do You Need Both?
In this article
  1. Node.js vs Express at a glance
  2. The same small API, with and without Express
  3. Plain Node.js
  4. Express
  5. What Express adds on top of Node.js
  6. Express 5: what changed from Express 4
  7. When to use plain Node.js and when to use Express
  8. Running an Express app on a server
  9. Common mistakes

Node.js and Express are not alternatives. Node.js is the runtime: the program that runs JavaScript on a server instead of in a browser. Express is a small web framework, installed with npm install express, that runs on top of Node.js and gives you routing, middleware and helpers for requests and responses. Every Express app is a Node.js app. You can build a web server with Node.js alone, but most people add Express (or a similar framework) once an app has more than a handful of routes.

Terminal showing node --version, npm view express version and a Node.js HTTP server returning status 200
Node.js 20 on Debian 13, the current Express version from npm, and a plain Node.js HTTP server answering 200

Node.js vs Express at a glance

Node.jsExpress
What it isA JavaScript runtime environmentA web framework for Node.js
How you get itInstall it on the server (installer, package manager or nvm)npm install express inside your project
What it gives youThe V8 engine, a standard library (files, network, HTTP, crypto) and npmRouting, middleware, body parsing, static files, error handling
Can it run on its own?YesNo, it needs Node.js
Used forWeb servers, command-line tools, scripts, build tools, workersWeb applications and APIs
Version we tested24.21.0 (LTS)5.2.1

The Node.js project describes Node.js as “an open-source and cross-platform JavaScript runtime environment” that runs the V8 engine from Google Chrome outside the browser, in a single process with non-blocking I/O (see Introduction to Node.js). The Express repository describes Express as a “fast, unopinionated, minimalist web framework for Node.js”.

The same small API, with and without Express

The easiest way to see the difference is to build one endpoint twice. We ran both versions in a node:24-alpine Docker container.

Plain Node.js

const http = require('node:http');

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/api/hello') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'Hello from plain Node.js' }));
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not found');
  }
});

server.listen(3000, () => console.log('Listening on port 3000'));

It works, and it needs no packages at all. But you check the method and URL yourself, set headers yourself and turn objects into JSON yourself. Add route parameters, form data, file uploads and error pages, and that if block grows quickly.

Express

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.use(express.json());
app.use(express.static('public'));

app.get('/api/hello', (req, res) => {
  res.json({ message: 'Hello from Express' });
});

app.get('/api/users/:id', (req, res) => {
  res.json({ id: req.params.id });
});

app.post('/api/echo', (req, res) => {
  res.status(201).json(req.body);
});

app.get('/api/fail', async (req, res) => {
  throw new Error('Something broke');
});

app.use((err, req, res, next) => {
  console.error(err.message);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(port, () => console.log(`Listening on port ${port}`));

What we got back:

GET  /api/hello     200  {"message":"Hello from Express"}
GET  /api/users/42  200  {"id":"42"}
POST /api/echo      201  {"name":"test"}
GET  /api/fail      500  {"error":"Internal server error"}
GET  /              200  <h1>Static file</h1>   (public/index.html)
GET  /nothing       404  (Express's default "not found" page)

What Express adds on top of Node.js

  • Routing: app.get(), app.post() and friends match the method and path for you, and :id style parameters land in req.params.
  • Middleware: functions that run in order before your route, added with app.use(). Logging, authentication, compression and security headers are all middleware.
  • Body parsing: express.json() and express.urlencoded() turn request bodies into req.body.
  • Static files: express.static('public') serves a folder of HTML, CSS and images.
  • Response helpers: res.json(), res.status(), res.redirect() and res.sendFile() set the right headers for you.
  • Error handling: a middleware with four arguments, (err, req, res, next), catches errors from any route.

Express stays small on purpose. It has no database layer, no user accounts and no admin panel; you pick npm packages for those.

Express 5: what changed from Express 4

Express 5 is now the default: npm view express dist-tags showed latest: '5.2.1', with Express 4 still published under latest-4 (4.22.3). Many tutorials were written for Express 4, so these differences from the official migration guide are worth knowing:

  • Node.js 18 or newer is required.
  • Async errors are caught. A rejected promise in a handler is passed to your error middleware. We ran the /api/fail route above on both versions: Express 5 answered with a 500, while Express 4.22.3 let the error escape and the whole Node.js process exited. On Express 4 you must wrap async code in try/catch and call next(err).
  • Wildcards need a name. app.get('*', ...) now stops the app at start-up with PathError [TypeError]: Missing parameter name at index 1: *. Use /*splat instead; in our test /files/*splat matched /files/a/b/c.txt and gave ["a","b","c.txt"].
  • Optional parameters use braces, for example /:file{.:ext} instead of /:file.:ext?.
  • Changed defaults: req.body is undefined when no body parser ran, express.urlencoded() defaults to extended: false, and express.static ignores dotfiles.
  • Removed methods: app.del() is now app.delete(), res.sendfile() is res.sendFile(), and res.send(body, status) becomes res.status(status).send(body).

According to the Express team, Express 2.x and 3.x are no longer maintained and should not be used at all.

When to use plain Node.js and when to use Express

SituationGood fit
A command-line tool, cron script or queue worker with no web interfaceNode.js alone
A tiny health-check or webhook receiver with one or two routesNode.js alone, or Express if you prefer the syntax
A REST API, a dashboard or a site with forms and loginsExpress
Learning how HTTP works under the hoodBuild it once with node:http, then switch to Express

Express is not the only web framework for Node.js. Whichever you choose, the server underneath is still Node.js, so the hosting steps below are the same.

Running an Express app on a server

An Express app is a long-running Node.js process, so it needs a server where you can keep processes running: a VPS or a dedicated server, not basic shared hosting. The Express project publishes production advice on its website; the checklist below follows it.

  1. Use an LTS release of Node.js. The Node.js release page says production applications should only use Active LTS or Maintenance LTS releases. Our tests used 24.21.0. To install it, see How to Install Node.js and NPM on Linux Servers.
  2. Set NODE_ENV=production. According to Express, this caches view templates and gives shorter error messages.
  3. Keep it running with systemd. Express recommends your init system for process management, so the app restarts after a crash or a reboot. A minimal unit file is shown after this list.
  4. Put a reverse proxy in front. Express recommends Nginx or HAProxy for TLS, compression and static files. Have the app listen on the loopback address only, with app.listen(3000, '127.0.0.1'), so it cannot be reached from outside except through the proxy. How to use Nginx as a reverse proxy with SSL walks through the Nginx side.
  5. Trim what you reveal. Express sends an X-Powered-By: Express header by default (we saw it on every response). app.disable('x-powered-by') removes it, though Express itself notes this only discourages casual attackers; its security guide also suggests the Helmet middleware for security headers.

A minimal systemd unit, based on the example in the Express production guide, saved as /etc/systemd/system/myapp.service:

[Unit]
Description=My Express app

[Service]
Type=simple
ExecStart=/usr/bin/node /srv/myapp/app.js
Environment=NODE_ENV=production
Restart=always

[Install]
WantedBy=multi-user.target

Check where node is installed with which node and adjust ExecStart, then run systemctl daemon-reload and systemctl enable --now myapp.

Common mistakes

  • Following an Express 4 tutorial on Express 5. A bare * route crashes the app at start-up. Check npm ls express to see which version you have.
  • Reading req.body without a parser. It is undefined until you add app.use(express.json()).
  • Starting the app with node app.js in an SSH session. It stops when you log out. Use systemd.
  • Opening the app port to the internet. Keep port 3000 on localhost and let the reverse proxy handle ports 80 and 443.

Ready to put a Node.js or Express app online? A KVM VPS gives you full root access to install Node.js, systemd services and Nginx your way. If you would rather have someone set it up, our engineers are available by the hour.

Share
Ithal P R

Ithal P R

Hi, I'm Ithal! I write tech and hosting blogs at Ucartz.com, where I share insights, tips, and the latest updates from the world of web hosting and technology. Whether it's about server setups, control panels, or tools to power your online presence, I break things down in a way that's easy to understand, even if you're just starting out.

Related articles

Start Building Today

Ready to Get Started?

VPS, dedicated servers, streaming and managed hosting with 24/7 support from the Ucartz team.