Stop using expressjs in 2023

Update Feb 10, 2024:

In this bloig post I suggested to go for Fastify as alternative to Express, There are a lot more options now.

Hono is another great alternative which supports multiple runtimes like node, deno, bun and Cloudflare workers.

Express is the most popular web framework for nodejs, It is downloaded 10s of millions of times each week. It was started within a year of nodejs launch and has always been a de-facto choice to build a server. it has a really simple and easy to reason about API, is stable and performs well. It has inspired web frameworks like fiber and tide because of its popularity and API simplicity.

Well if everything about express is so great then why should we stop using it, you ask? #

To understand this we have to get into its history. The latest Express version is v4.18.2 and v4.0.0 was released in 2014, so in last 9 years no new major version has been released. There have been several efforts to release Express 5.0 but that never materialised. So it's safe to assume Express 5.0 will never see the light of day.

Why do we need a new major version of express 4.x is already stable? #

There are a few missing features in express 4.x which can lead to foot guns

1. Promises/Async function support #

Express does not support Javascript promises. In 2014 when Expess 4.0.0 was released Promises had not landed in ECMAScript spec nor in nodejs. Express embraced callbacks for all async work.

if we write an async handler like this

const express = require('express');

const app = express();

async function getData() {
  return 'hello world';
}

app.get('/', async (req, res) => {
  const data = await getData();
  res.send(data);
});

app.listen(3000, () => {
  console.info('server running on port 3000');
});

everything seems to be working fine, But there is a big issue here. If the async code throws errors unhandled exception is thrown.

async function getData() {
  throw new Error("Something is wrong");
}

app.get('/', async (req, res) => {
  const data = await getData(); // unhandled error
  res.send(data);
});

We can technically prevent this by wrapping function call using try/catch block, but this is not required in the case of synchronous code. If the error is thrown in sync code it is automatically handled by express. So express behaves differently in case of sync and async execution.

function getData() {
  throw new Error("Something is wrong");
}

app.get('/', (req, res) => {
  const data = getData(); // error is caught by express
  res.send(data);
});

The same is the issue while creating async middleware, Error is not caught in middleware and next is called without any error.

async function getData() {
  throw new Error("Something is wrong");
}

app.use(async (req, res, next) => {
  const data = await getData();
  next()
});

2. Typescript support #

Express has decent Typescript support using @types/express packages. It is not as good as a modern framework like fastify. In express we can type-check URL params, response body, Request body and query parameters


type Params = {
  id: string;
};
type ResBody = {
  data: "Hello world";
};
type ReqBody = {}
type ReqQuery = {}

app.get<Params, ResBody, ReqBody, ReqQuery>("/:id", (req, res) => {
  res.status(200).json({ data: "Hello world" });
});

Fastify goes a step beyond and it can type-check response as per status code(so different response for valid response and error)

import fastify from "fastify";

const app = fastify();

type Querystring = {
  username: string;
  password: string;
};

type Headers = {
  "h-Custom": string;
};

type Reply = {
  200: { success: boolean };
  302: { url: string };
  "4xx": { error: string };
};

type Params = {
  id: string;
};

app.get<{
  Querystring: Querystring;
  Headers: Headers;
  Reply: Reply;
  Params: Params;
}>("/:id", async (request, reply) => {
  const { username, password } = request.query;
  const customerHeader = request.headers["h-Custom"];
  // do something with request data

  // chaining .statusCode/.code calls with .send allows type narrowing. For example:
  // this works
  reply.code(200).send({ success: true });
  // but this gives a type error
  //   reply.code(200).send('uh-oh');
  // it even works for wildcards
  reply.code(404).send({ error: "Not found" });
  return `logged in!`; // this also doesn't work
});

app.listen({ port: 3000 });

3. Schema validation #

In express our request validation need to be done manually. Fastify supports a native schema validation. If the request is not as per schema an error response is sent.

const schema = {
  params: {
    type: 'object',
    properties: {
      par1: { type: 'string' },
      par2: { type: 'number' }
    }
  },

  headers: {
    type: 'object',
    properties: {
      'x-foo': { type: 'string' }
    },
    required: ['x-foo']
  }
}

fastify.post('/the/url', { schema }, handler)

4. Performance #

Express performance is good for most applications. Most devs would say that I/O is the real bottleneck in most cases. While that is somewhat true but a low overhead framework can help improve the response latency. As per TechEmpower benchmark fastify has half the latency compared to express on average. For most apps req/sec benchmark may not indicate anything but latency improvements will be seen in real world even if we are doing 2 req/sec.

So considering all these drawbacks it is probbaly a good idea to ditch express for a modern framework like fastify.


Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated ! For feedback, please ping me on Twitter.

Published