Stop using Express.js

January 02, 2024

Historically, Express.js has been the go-to framework for setting up HTTP servers in Node.js due to its simplicity and extensive use. However, with the evolution of Node.js and its ecosystem, it’s time to reconsider this default choice, particularly for new projects.

What’s wrong with Express.js ?

1. No proper async/await handling

  • Express.js struggles with handling asynchronous operations efficiently. Async/await, a feature now commonplace in JavaScript for handling asynchronous code, does not integrate seamlessly with Express.js. To fix this issue in Express.js, you would typically wrap each middleware and route handler in a try/catch block. However, this approach is error-prone. In future Node.js versions, such rejections are treated as fatal errors and can crash the application. This approach is also non featured in Express.js documentation.
const app = express();

// Route handler with bad promise rejection handling ❌
// It can lead to memory leaks
app.get("/error", async (req, res) => {
  const fetchResponse = await fetch("https://httpstat.us/500");
  const text = await fetchResponse.text();
  res.send(text);
});

// Route handler with good promise rejection handling ✅
// Express need a try/catch block to handle promise rejections
app.get("/error", async (req, res) => {
  try {
    const fetchResponse = await fetch("https://httpstat.us/500");
    const text = await fetchResponse.text();
    res.send(text);
  } catch (error) {
    // Error handling is necessary with try/catch
    console.error(error);
    res.status(500).send("An error occurred");
  }
});
  • The Express team, recognizing the limitations of Express.js in handling asynchronous operations, created Koa. Although Koa fixed async/await and has a new API to handle HTTP requests, it didn’t replace Express because the community of Express was too huge and, like today, people are ignoring Express.js issues.

2. Performance: 2x to 7x slower than alternatives

Benchmarks show Express.js lagging behind other frameworks in terms of requests per second. For instance, in a benchmark using autocannon, Express.js handled 14,200 requests/sec, while Fastify handled 77,193 requests/sec. This significant difference illustrates the potential performance gains of choosing a more modern framework with a very similar API.

3. Not maintained

Express.js has not seen significant updates in recent years. This stagnation means it lacks several modern features that newer frameworks offer, which can lead to a more efficient and effective development process. Express 5 was announced in 2014, 10 years ago, but as of 2024, it is still in beta. The last release of v5 beta was in February 2022, which was 2 years ago. Similarly, the latest release of Express.js was in October 2022, 2 years ago.

The alternative to Express: Fastify

Alternatives

Express.js is a low-scope HTTP framework. Let’s compare other NodeJS low-scope frameworks together:

FrameworkVersionRouter?Requests/secSafe async/await?Is maintained?
Express.js4.17.314,200
hapi20.2.142,284
Restify8.6.150,363
Koa2.13.054,272
Fastify4.0.077,193
------
http.Server16.14.274,513

Benchmarks ‘Requests/sec’ runned from https://github.com/fastify/benchmarks

From these benchmarks, we can see that Fastify got huge advantages compared to the alternatives: 7x times than Express and got all features we need. The greatest asset of Fastify is that it’s maintained by Matteo Collina, a TSC member at NodeJS. Matteo Collina has indeed made significant contributions to Node.js, particularly in areas related to HTTP, streams, and diagnostics. Obviously Fastify would always follow NodeJS evolution.

Migration from Express to Fastify

Below some snippets of most features we need from a low-scope HTTP framework comparing Express and Fastify syntax.

Create and start server

import Express from "express";

// Create an instance of express
const express = Express();

// Start the server
const port = 3000;
express.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});
import Fastify from "fastify";

// Create an instance of Fastify
const fastify = Fastify();

// Start the server
const port = 3000;
await fastify.listen({ port });
console.log(`Server is running at http://localhost:${port}`);

Basic route for GET request

express.get("/", (req, res) => {
  res.send({
    message: "Welcome to my app!",
  });
});
fastify.get("/", async (request, reply) => {
  return {
    message: "Welcome to my app!",
  };
});

Route with URL parameters

express.get("/users/:userId", (req, res) => {
  res.send(`User ID is ${req.params.userId}`);
});
fastify.get("/users/:userId", async (request, reply) => {
  return `User ID is ${request.params.userId}`;
});

POST request with body parsing

// Middleware to parse JSON bodies
express.use(Express.json());

express.post("/submit", (req, res) => {
  console.log("Received data:", req.body);
  res.send("Data received successfully");
});
fastify.post("/submit", async (request, reply) => {
  console.log("Received data:", request.body);
  return "Data received successfully";
});

Route with query parameters

express.get("/search", (req, res) => {
  const query = req.query.q;
  res.send(`Search query is ${query}`);
});
fastify.get("/search", async (request, reply) => {
  const query = request.query.q;
  return `Search query is ${query}`;
});

Static file serving

Serving files from ‘public’ directory

// Middleware for static files
express.use(Express.static("public"));
// Static file serving (requires @fastify/static)
import path from "path";
import { fileURLToPath } from "url";

fastify.register(import("@fastify/static"), {
  root: path.join(path.dirname(fileURLToPath(import.meta.url)), "public"),
});

Error handling

express.use((err, req, res, next) => {
  console.error("An error occurred:", err);
  res.status(500).send("Internal Server Error");
});
fastify.setErrorHandler(function (error, request, reply) {
  console.error("An error occurred:", error);
  reply.status(500).send("Internal Server Error");
});

Async/Await Error handling

// Express needs a try/catch block to handle promise rejections
express.get("/error", async (req, res) => {
  try {
    const fetchResponse = await fetch("https://httpstat.us/500");
    const text = await fetchResponse.text();
    res.send(text);
  } catch (error) {
    console.error(error);
    res.status(500).send("An error occurred");
  }
});
// Fastify got native promise rejection handling
fastify.get("/error", async (req, res) => {
  const fetchResponse = await fetch("https://httpstat.us/500");
  const text = await fetchResponse.text();
  res.send(text);
});

Migrate a large Express project to Fastify

Using @fastify/express, this plugin adds full Express compatibility to Fastify, it exposes the same use function of Express, and it allows you to use any Express middleware or application.

Conclusion

Adopting Fastify instead of Express.js is a significant advancement in Node.js web development. With its better performance, async/await support, ongoing maintenance, and compatibility with current JavaScript features, Fastify is an excellent choice for new and existing projects.

Benchmarks and code comparisons show Fastify as a user-friendly upgrade from Express.js, easing the transition for developers. Node.js developers, particularly those beginning new projects, should consider Fastify to improve performance, streamline code management, and stay aligned with JavaScript and Node.js community trends.

Next time you see somebody that will use Express.js for his next Node.js HTTP server project please share him this article.