DEV Community

Cover image for Express.js adoption guide: Overview, examples, and alternatives
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Express.js adoption guide: Overview, examples, and alternatives

Written by Antonello Zanini✏️

Express.js is a Node.js framework for creating maintainable and fast backend web applications in JavaScript. In the fast-paced world of web development, Express has become a leading technology for building modern web applications due to its flexible and minimalist architecture.

Join us in this adoption guide and get ready to learn everything you need to know about Express and when to use it in your projects!


Introduction to Express.js

Express is a versatile, unopinionated, and minimalistic web framework for building scalable and robust backend applications in Node.js. Over time, it has become so popular that most resources about Node.js actually refer to Express.

Let's now take a step back and explore its history before seeing how it works and how to get started with it.

Express.js history: From zero to hero

The history of Express began in 2010, during the early days of Node.js. While Node.js brought JavaScript to server-side programming with an asynchronous and event-driven approach, it lacked key features like routing, templating, middleware, and robust error handling.

Legendary open source developer TJ Holowaychuk decided to make the first move. Inspired by the Ruby philosophy embodied in Sinatra, he sought to create a web framework that was both simple and expressive. He borrowed some concepts from other web frameworks and created Express.

As the Node.js ecosystem flourished, so did Express. In 2014, the management rights of the technology were acquired by StrongLoop, which caused discontent and concerns about the project becoming more commercially oriented.

In response, some developers forked Express to create alternative frameworks like Koa. StrongLoop was later acquired by IBM in 2015, which decided to place Express in the Node.js Foundation incubator to ensure long-term sustainability and community involvement.

Express gained popularity and has become a cornerstone of the MEAN, MERN, and MEVN stacks. Since the release of its fourth major version, Express.js has undergone significant enhancements. Version 4 introduced a new router, improved error handling, and a more modular architecture.

The community is currently working on Express 5, which will introduce features like native support for promises and async/await programming.

Express remains a pillar of the Node.js ecosystem, shaping the evolution of web frameworks and influencing projects both within and outside its community.

Further reading:

How Express.js works

Express.js operates as a middleware-based library designed for building robust backend applications in Node.js. At its core, the framework revolves around the concept of routes.

Routes define how the application responds to client requests for specific URLs and HTTP methods. They can perform several operations, from serving static files to executing intricate business logic. That means an Express application can be both an API backend and a web server for hosting static and dynamic web pages.

The framework has a modular architecture, allowing developers to seamlessly define and register middleware functions. These can be applied across all routes or selectively to specific endpoints to handle specific tasks, such as API key authentication and error handling.

This unique combination of routing and middleware makes Express.js an adaptable solution for building powerful Node.js applications.

Further reading:

Setting up an Express.js project

Starting an Express application is a simple process that takes just a few steps.

First, initialize a Node.js project:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Then, add Express to the project's dependencies:

npm install express
Enter fullscreen mode Exit fullscreen mode

Next, create an index.js JavaScript file and define a basic Express server with a single route:

const express = require('express')

// initialize an Express application
const app = express()

// define a simple "/api/hello-world" route
app.get('/api/hello-world', (req, res) => {
  res.json('Hello, World!')
})

// start the server
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`)
})
Enter fullscreen mode Exit fullscreen mode

Execute the command below to launch the app:

node index.js
Enter fullscreen mode Exit fullscreen mode

The http://localhost:3000/api/hello-world endpoint will now respond with the following message:

'Hello, World!'
Enter fullscreen mode Exit fullscreen mode

Great, you just set up an Express project!


Why choose Express.js?

Here, you will examine how Express behaves in the main aspects to consider when evaluating a web framework.

Performance

Express.js is well-known as a fast and responsive technology, with endpoints responding in just a few milliseconds on average. The main reason is its lightweight architecture.

It also relies on the non-blocking I/O nature of Node.js to efficiently handle concurrent requests. This enables Express to deal with thousands of simultaneous requests without significant performance degradation.

The main performance limitation of Express is that Node.js uses only one CPU core by default. That can become a problem with CPU-intensive tasks, which can easily saturate the resources of the underlying machine.

After all, if your server machine has multiple CPUs, you would like to use all of them. The solution is called “clustering,” a technique that allows the creation of child processes that run simultaneously by sharing the same server port.

Further reading:

Developer experience

Express.js is also known for its simplicity, minimalism, and flexibility. In particular, it comes with a straightforward API that provides building blocks such as routing, middleware, and other components to get a web application up and running quickly.

Express offers an unopinionated approach to web development, which means you’re free to structure your application as you prefer. That’s a nice option to have, but may also scare off some developers.

Following best practices for organizing your Express.js project structure enables you to benefit from the flexibility of Express without becoming uncertain or overwhelmed.

Further reading:

Bundle size

As of this writing, the express npm package has an unpacked size of about 200kB. This small size is due to the design philosophy of Express.js, which involves a lean and minimal core.

The minimal core contributes to a lighter overall bundle size compared to more feature-heavy frameworks. For example, a bundled Spring Boot application with basic web capabilities weighs about 18 MB.

On the other hand, having a lightweight core means you might have to install additional dependencies based on your project’s requirements. While that impacts the overall bundle size of your backend application, the flexibility to include only what is really required makes Express a great composable framework.

Community and ecosystem

According to Statista, Express is one of the most used backend web frameworks in the world. Nearly 20 percent of developers surveyed reported using it. That makes it second only to Node.js — the technology Express is based on.

Since Node.js is estimated to have several million developers worldwide, it is safe to say that the Express community boasts millions of users as well. No wonder the official Express repository on GitHub has more than 60k stars!

On top of that, Express is backed by a rich ecosystem of libraries. You can use many of the libraries available via npm, which lists over 1.3 million packages. The “Express” tag alone on npmjs.com counts more than six thousand libraries.

Learning curve

The Express API and syntax are designed to be accessible to both beginners and experienced developers. The learning curve is gentle, and it usually takes only a few weeks to master its main aspects, such as routing and the middleware mechanism. After all, it would not be so popular if it was difficult to learn.

That being said, some extra issues may arise when integrating it with TypeScript. To avoid that, follow our tutorial on how to set up TypeScript with Node.js and Express.

Documentation

For such a popular framework, the official Express documentation is not the best. The project is run only by volunteers, which is clear from the documentation site. It does a decent job providing guidance on features and usage, but it could be much better.

In contrast, the official API reference for Express has improved a lot over time and is now a complete and comprehensive resource.

What makes the difference is the vast community supporting the technology. There are thousands of questions and answers on Stack Overflow and many useful, in-depth blog posts. It’s quite difficult to come across problems that haven’t already been documented and addressed by other developers.

Integrations

The unopinionated nature of Express.js implies that it doesn’t impose any specific technology. For instance, it integrates seamlessly with databases like MySQL and PostgreSQL, various template engines, and web coding tools like Prettier and ESlint.

These are just a few examples, but keep in mind that Express can integrate with many other technologies. The main advantage of this characteristic is that developers are free to adopt the tools, libraries, and modules best suited to their needs.

Further reading:


Key Express.js features to know

Let’s dig into the main features of Express.js by describing them, learning how they work, and demonstrating their use.

Defining RESTful endpoint routes

In a backend server, routing refers to how the application endpoints respond to client requests. To define routes, the Express app object exposes the following methods corresponding to the HTTP methods:

  • app.get(): To define GET requests
  • app.post(): To define POST requests
  • app.put(): To define PUT requests
  • app.patch(): To define PATCH requests
  • app.delete(): To define DELETE requests

These routing methods accept an endpoint string and a callback function. The callback is executed when the Express application receives a request that matches the specified endpoint string and HTTP method.

For example, you can define a GET /api/users/:id endpoint as follows:

app.get('/api/users/:id', (req, res) => {
  // read the id parameter from the URL
  const userId = req.params.id

  // retrieve paginated users
  const users = getUsers(pageSize, pageOffest) // getUsers() is omitted for brevity...

  // serialize the paginated users to JSON format
  res.json({ user: user })
})
Enter fullscreen mode Exit fullscreen mode

The params property of the req object exposes the values of the named parameters specified in the REST endpoint. Thus, when calling the API with /api/users/1 endpoint, the req.params.id value is 1.

To access the query parameters, use the query property instead. req.json() serializes the JavaScript object passed as a parameter into JSON and returns it to the client. To return data in other formats, you can use req.send() as specified in the docs.

Similarly, here’s how you can define a POST /api/users endpoint that accepts a body with JSON data to create a new user:

app.post('/api/users', (req, res) => {
  // extract the information from the body 
  const { username, email } = req.body

  // create a new user
  createUser(username, email) // createUser() is omitted for brevity...

  // return a "201 Created" empty response
  res.status(201).json()
})
Enter fullscreen mode Exit fullscreen mode

By default, req.body is always undefined. It gets populated only when you use body-parsing middleware such as express.json() or express.urlencoded(). If you‘re unfamiliar with Express middleware, we’ll cover it later in this guide.

Keep in mind that you can customize the routes, middleware, and handlers in your project based on your specific API requirements. Also, you can organize your routes in controller files for better maintainability and readability.

Connecting to databases

Connecting to a database to retrieve and store data is a crucial aspect of backend development. Express.js does not enforce a specific database choice, allowing developers to connect to any database technology.

Manually managing database connections is tricky, and developers usually rely on object-relational mapping (ORM) or object-document mapping (ODM) libraries. The vast Express ecosystem offers two notable options for facilitating database interactions:

  • Sequelize: An ORM designed to deal with relational databases, such as MySQL, PostgreSQL, and SQL Server. Sequelize eliminates the need to write raw SQL queries by providing an abstraction layer to interact with the underlying database via JavaScript objects and methods. Sequelize also works with TypeScript
  • Mongoose: An ODM tailored to work with MongoDB, the most popular NoSQL database. Mongoose streamlines the interaction with MongoDB thanks to its approach to schema-based query building

The steps required to get started with these technologies are:

  1. Install the library — in the case of Sequelize, you also need to install the database driver
  2. Import the ORM/ODM library into your Express application
  3. Use the library to connect to your database by specifying the required connection information
  4. Map your database tables/collections into JavaScript model objects
  5. Use the model objects to retrieve data in your routing handlers

For example, after defining the User model in Sequelize, retrieving all users from the database is as simple as writing the following:

const users = await User.findAll() 
Enter fullscreen mode Exit fullscreen mode

Middleware modules

An Express middleware is a function that has access to the request object (req), the response object (res), and the function to proceed in the request-response cycle (next).

Middleware modules can perform various tasks while an HTTP request is processing, such as modifying the request or response objects, terminating the request-response cycle, or calling the next middleware function. Note that the same endpoint can be associated with more than one middleware.

Here is how you can define a middleware function in Express:

const loggingMiddleware = (req, res, next) => {
  // log the body of the request
  console.log(req.body)

  // call the next middleware
  next() 
}
Enter fullscreen mode Exit fullscreen mode

The next() function at the end of the middleware definition passes control to the next function in the endpoint definition stack. If a middleware function does not call next(), the request-response cycle is terminated and no further functions in the stack are executed.

Another way to terminate the request-response cycle is to send a response to the client using methods like res.send() or res.json().

A middleware can be registered globally to all routes with app.use():

app.use(loggingMiddleware)
Enter fullscreen mode Exit fullscreen mode

You can also choose to only pass a middleware to some endpoints as below:

app.get('api/v1/users/:id', loggingMiddleware, /* other middleware..., */ (req, res) => {
 // ...
})
Enter fullscreen mode Exit fullscreen mode

Bear in mind that middleware functions are executed in the order they are registered using app.use() or in the specific routing methods.

Express.js supports two types of middleware:

  • Request middleware: Functions that have access to the req, res, and next parameters. They usually perform tasks such as logging, authentication, data parsing, and more
  • Error middleware: Functions that have access to the err, req, res, and next parameters. They are called only when an error occurs and are specifically designed to handle errors

Some of the most used Express middleware libraries are:

  • cors: To define cross-origin policies for requests between browsers and the server
  • multer: To deal with multipart/form-data requests, which are generally used for file uploads
  • connect-timeout: To define a timeout to requests, terminating them if they take too long to process
  • helmet: To secure your application by setting standard-based security HTTP headers
  • express-rate-limit: To protect against DoS attacks by limiting the number of requests the same client can make in a limited time span

Further reading:

Using cookies and managing sessions

HTTP is a stateless protocol, meaning it doesn’t retain information about the state of the communication between client and server. Cookies were introduced to address that limitation, allowing both the client and the server to maintain state between requests.

In web development, cookie management is essential for various purposes, such as tracking user sessions.

The most common Express middleware library for handling cookies is cookie-parser. This package provides a middleware function that parses the Cookie header and populates req.cookies with an object containing the cookie’s <name, value> pairs.

After installing this package, register the cookie-parser middleware as below:

const express = require('express')
const cookieParser = require('cookie-parser')
const app = express()

app.use(cookieParser())
Enter fullscreen mode Exit fullscreen mode

You can now access the cookie property from req as in the following snippet:

app.post('/set-cookie', (req, res) => {
  // instruct the client to set a cookie
  // for the specified domain
  res.cookie('user', 'John Doe', { domain: '.example.com', path: '/' })
  res.send('Cookie set!')
})

app.get('/hello', (req, res) => {
  const username = req.cookies.user // "John Doe"
  res.send(`Hello, ${username}!`) // "Hello, John Doe"
})
Enter fullscreen mode Exit fullscreen mode

res.cookie() is the method provided by Express to define a cookie.

Another useful middleware package that follows a similar approach to cookie-parser is express-session. This library provides a middleware function that adds a session property to req to store session data.

The express-session middleware function defines a cookie that only contains the session ID, while the session data is stored directly on the server. That is a great approach to secure session management in Express.

Further reading:

Error handling

By default, Express.js comes with a simple error handling middleware. This intercepts and handles errors that occur during the request-response cycle, returning a HTML status code of 500 with the message Internal Server Error.

To override that behavior, you can define a custom error middleware function as in the following example:

app.use((err, req, res, next) => {
  // log the error object
  console.error(err)

  // return a 500 response with a custom message
  res.status(500).json('Something went wrong!')
})
Enter fullscreen mode Exit fullscreen mode

Bear in mind that Express doesn’t automatically catch all errors occurring in route handlers and middleware functions — you need to ensure this manually.

Errors that occur in synchronous code are intercepted and passed by default to the error-handling middleware. For errors occurring in asynchronous code, you must instead pass them to the next() function as shown below:

app.get('/api/users/stats', (req, res, next) => {
  fs.readFile('/stats.csv', (err, data) => {
    if (err) {
      // propagate the error to the error-handling controller
      next(err)
    } else {
      res.send(data)
    }
  })
})
Enter fullscreen mode Exit fullscreen mode

This is the only way Express will catch and process them.

User authentication

User authentication is a fundamental aspect of building secure web applications. Express offers flexibility in implementing authentication, supporting both of the following techniques:

  • Client-side token-based authentication: A stateless approach to authentication where the server does not store any session information about the user. Instead, each request from the client includes a JWT-like token containing the necessary information to authenticate the user. The server validates the token on each request, making it a scalable solution suitable for modern web applications and microservices architectures
  • Server-side session-based authentication: A stateful approach to authentication where the server stores session information about the authenticated user. When a user logs in, the server creates a session ID and stores it either in a database or an in-memory cache. Subsequent requests from the user include this session ID for authentication.

You can explore the implementation details along with the pros and cons of these approaches in our guide on Node.js server-side authentication.

Serving static files

You can also use Express.js to serve static files such as images, CSS stylesheets, JavaScript files, HTML files, or any other assets. The framework provides a built-in middleware called express.static(). This accepts a string argument with the path to the root directory from which to serve static assets.

Use express.static() as shown below:

const express = require('express')
const path = require('path')

const app = express()
// serve static files from the 'public' directory
app.use(express.static(path.join(__dirname, 'public')))
Enter fullscreen mode Exit fullscreen mode

Express will now publicly expose all files under the local public folder. So, assume your public directory contains the following files:

public
├── images
│   └── bg.jpg
├── css
│   └── style.css
├── js
│   └── app.js
├── index.html
└── robots.txt
Enter fullscreen mode Exit fullscreen mode

Start the development server on port 3000 and you will be able to access them like so:

  • http://localhost:3000/images/bg.jpg
  • http://localhost:3000/css/style.css
  • http://localhost:3000/js/app.js
  • http://localhost:3000/index.html
  • http://localhost:3000/robots.txt

To serve static files from multiple folders, register express.static() multiple times on different paths.

Templating

Templating refers to the process of dynamically generating HTML content by populating static structures with dynamic data. Express.js provides templating capabilities to create flexible and reusable HTML views to enhance server-side rendering.

Some of the most popular template engines supported by Express are:

  • EJS: Offers a simple and familiar syntax, embedding JavaScript within HTML to generate dynamic content. It provides seamless integration with Express and is suitable for developers familiar with JavaScript
  • Pug (formerly Jade): Reduces redundancy by using indentation instead of tags and is known for its concise and clean syntax. It promotes a more readable template structure, making it an excellent choice for those who prefer a minimalist approach
  • Mustache: A logic-less template syntax, focusing on simplicity and ease of use. It encourages a clean separation between logic and presentation for scenarios where simplicity and readability are paramount
  • Eta: Provides blazing-fast embedded JavaScript templating engine. It is a lightweight library written in TypeScript that emphasizes great performance, customization, and small bundle size
  • Nunjucks: Provides a powerful and extensible templating engine with features like template inheritance and macros. It supports asynchronous loading, making it suitable for complex applications requiring advanced templating capabilities

Check out the documentation for a complete list of Express template engines.

Further reading:

Dealing with form data

By default, Express can interpret application/x-www-form-urlencoded requests after registering the express.urlencoded middleware:

app.use(express.urlencoded({ extended: true }))
Enter fullscreen mode Exit fullscreen mode

You will now find the body data of an application/x-www-form-urlencoded request in req.body.

However, that approach does not work on multipart/form-data requests, which are primarily used for uploading files. In this case, you need the Node.js middleware library multer instead.

Further reading:

Project scaffolding

Scaffolding involves setting up the directory structure, files, and configurations of a new project. Even though setting up a new Express.js project is not complex, organizing it correctly and integrating it with popular libraries involves boilerplate operations.

For this reason, Express supports project scaffolding via the following libraries:

  • Express Generator: A command-line tool for quickly generating the basic structure of an Express.js application. It provides a set of pre-configured templates for routes, views, and other components
  • Yeoman: A robust scaffolding tool that supports various generators, including those for Express.js projects. It enables developers to create and share generators for different project structures and configurations
  • Cookiecutter: A templating tool that facilitates project scaffolding based on project templates. There are templates available also for Express.js projects, allowing for quick setup with predefined structures and configurations

These tools improve productivity, help save time, and make it easier to organize code, manage dependencies, and adhere to best practices.


Use cases for Express.js

These are some of the most common use cases of Express.js:

  • API development: As a minimal, flexible, and scalable framework, it represents a great choice for developing microservices and RESTful API backends. This is probably the main use case for which Express is so popular
  • Web server implementations: Express.js is well-suited for serving web applications, whether they are static sites, dynamic web pages, or single-page applications. That is possible because it supports both static routing and templating.
  • Real-time applications: By integrating frameworks like Socket.io, Express supports the development of real-time applications such as chat applications, online gaming platforms, and collaborative tools
  • Integration in widely used technology stacks: Express.js is a key component in popular technology stacks like MEN, MERN, MEAN, and MEVN, providing a solid foundation for building modern, scalable, and efficient web applications

The JavaScript web framework is so flexible that it adapts to many more real-world scenarios and applications. No wonder Express.js has been embraced by well-known enterprises such as Netflix, Uber, Fox Sports, Accenture, and IBM.

Further reading:


Express.js vs. similar frameworks

Express.js is not the only web framework available, and it has many competitors. It is time to see how it compares against other similar technologies!

The comparison aspects we’ll focus on in this section will include features, performance, community, and documentation.

Express.js vs. Spring Boot

Spring Boot is a powerful Java-based framework that simplifies the development of highly robust and scalable applications. In comparison to Express:

  • Features: Spring Boot comes with many more built-in features than Express, but at the cost of being much heavier and more opinionated about the development approach to follow
  • Performance: Spring Boot guarantees high performance in large-scale enterprise applications, especially when dealing with CPU-intensive tasks. Meanwhile, Express.js performs better on smaller projects and non-CPU-intensive tasks due to its lightweight nature
  • Community: Spring Boot can rely on the active and large Java community, ensuring strong support and a vast pool of resources. At the same time, Express.js boasts many more active developers and libraries
  • Documentation: The Spring Boot official documentation is comprehensive and always up-to-date, but Express has more online resources

I’d recommend opting for Spring Boot for large-scale enterprise applications, but for simpler projects, I prefer Express.js for its composability, flexibility, and performance.

Express.js vs. Laravel

Laravel is an elegant full-stack PHP framework known for its expressive syntax and developer-friendly features. Let’s see how it stacks up to Express:

  • Features: Laravel is an all-in-one solution that provides features like authorization, authentications, migrations, Eloquent ORM for streamlined database interactions, and the Blade templating engine for dynamic views. In comparison, Express.js has a more minimalist approach and promotes integration with different libraries and technologies
  • Performance: Laravel ensures good performance with the right optimizations, but Express tends to perform better while providing a similar level of scalability
  • Community: Laravel has a vibrant community, but PHP is not the most popular language for web developers, to say the least. Express is instead based on a strong and growing JavaScript community
  • Documentation: Laravel's official documentation is extensive and complemented by the Laracasts video tutorials. Conversely, Express.js has better-documented online resources because of its larger community

Choose Laravel for a more structured approach to web development or if you are looking for a full-stack PHP framework. Go for Express.js for faster responses, a larger community, and more freedom.

Express.js vs. Flask

Flask is a lightweight Python web framework known for its simplicity and modularity. Here’s how Express and Flask compare:

  • Features: Flask embraces a micro-framework design approach that shares the same minimalist idea with Express. The main difference is that the Flask extension-based architecture provides more modular control
  • Performance: Express.js is much faster than Flask because Node.js is much faster than Python
  • Community: Express is more popular than Flask, but both count millions of developers in their communities and thousands of libraries
  • Documentation: In both cases, the official documentation is clear, concise, and based on community support. Flask's documentation may be a bit more complete than that of Express

Flask and Express.js are similar frameworks. Flask is great for very small projects because of its extreme modularity, while Express is overall faster and more suitable for general use cases.

Express.js JavaScript alternatives

Other Express alternatives based on Node.js are:

  • Fastify: A web framework for Node.js focused on speed and low overhead, ideal for building highly efficient APIs
  • Koa: A minimalist and expressive web framework for Node.js, designed by the creators of Express.js and emphasizing middleware composition
  • Nest: A progressive TypeScript framework for building scalable and maintainable server-side applications, built on top of Express.js
  • Hapi: A rich framework focused on configuration-driven development and extensibility for building applications and services in Node.js
  • DerbyJS: A full-stack MVC web framework for building real-time, collaborative applications with a focus on seamless data synchronization

To see what these frameworks have to offer and how they compare with Express, read our in-depth comparison.


Conclusion

In this article, we covered what Express.js is and how it became one of the most popular web frameworks on the planet. We explored how it works, what its main features are, and how it compares to other popular web technologies. Express expertise unlocked!

The aim of this overview was to guide you in your consideration of Express.js for your next project. If you have any further questions about what Express is best for and how to use it efficiently, feel free to comment below.

Further reading:


Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (0)