Complete Guide to Express.js: Everything You Need to Know

Express.js cover image

What is Express.js?

Express.js is basically a minimal framework for Node.js that allows you to build web applications and APIs. It was created to simplify the process of creating web servers in JavaScript, which would be much more verbose and complicated with pure Node.js.

What makes it so popular is its simplicity and flexibility. It doesn’t impose a rigid structure like other frameworks, but gives you the necessary tools to build what you want, how you want. It’s a bit like having a set of Lego bricks instead of an already assembled model.

If we compare it with other frameworks like Koa, Fastify or NestJS, Express turns out to be simpler and more straightforward. Koa is actually created by the same developers as Express and offers a more modern approach with the use of async/await. Fastify is optimized for performance and is faster than Express in many benchmarks. NestJS, on the other hand, is a complete framework that uses TypeScript and is inspired by Angular, so it’s more structured and suitable for enterprise projects.

Prerequisites

Before diving into Express.js, you should have a basic knowledge of JavaScript and especially Node.js. You don’t have to be an expert, but you should know how callbacks, promises, and maybe even async/await work.

It’s also important to be familiar with HTTP concepts and REST APIs. You need to understand what HTTP methods like GET, POST, PUT, and DELETE are, and how HTTP requests and responses work.

1. Installation and Initial Setup

Setting up an Express.js project

Getting started with Express is really simple. First, you need to create a new Node.js project:

mkdir my-express-project 
cd my-express-project 
npm init -y

Then install Express:

npm install express

And you can create an app.js file with a basic server:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

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

That’s it! You’ve just created a web server that responds with “Hello World!” when you visit the homepage.

Structure of an Express.js project

Express doesn’t impose a specific structure, but there are some conventions that are good to follow. A common structure is based on the MVC (Model-View-Controller) pattern:

/project
  /controllers
  /models
  /views
  /routes
  /middlewares
  /public
  app.js
  package.json

For more complex projects, you can use express-generator, a tool that automatically creates the basic structure:

npm install -g express-generator
express project-name

2. Routing in Express.js

Defining Routes

Routing is one of the fundamental aspects of Express.js. Routes allow you to define how the application responds to client requests.

// Simple GET route
app.get('/users', (req, res) => {
  res.send('List of users');
});

// POST route
app.post('/users', (req, res) => {
  res.send('User created');
});

// Route with parameter
app.get('/users/:id', (req, res) => {
  res.send(`Details of user ${req.params.id}`);
});

Middleware in Routes

Middleware are functions that have access to the request object (req), the response object (res), and the next function in the request-response cycle. They can execute code, modify the req and res objects, end the cycle, or call the next middleware.

// Logging middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

// Middleware for a specific route
app.get('/admin', (req, res, next) => {
  // Check if the user is authenticated
  if (!req.isAuthenticated()) {
    return res.redirect('/login');
  }
  next();
}, (req, res) => {
  res.send('Admin panel');
});

Modular Routers

To better organize your code, Express allows you to create modular routers:

// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.send('List of users');
});

router.get('/:id', (req, res) => {
  res.send(`Details of user ${req.params.id}`);
});

module.exports = router;

// app.js
const usersRouter = require('./routes/users');
app.use('/users', usersRouter);

3. Handling Requests and Responses

Request (req) and Response (res)

The req and res objects contain all the information about the request and the methods to send the response.

app.get('/products', (req, res) => {
  // req.params: route parameters
  // req.query: query string parameters (?name=value)
  // req.body: request body (for POST, PUT, etc.)
  
  // Sending a response
  res.send('Simple text');
  // or
  res.json({ name: 'Product', price: 19.99 });
  // or
  res.status(404).send('Not found');
});

Data Handling

To handle data sent in requests, you need to use parsing middleware:

// For parsing application/json
app.use(express.json());

// For parsing application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));

// For file uploads
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('file'), (req, res) => {
  res.send('File uploaded');
});

4. Advanced Middleware

Third-Party Middleware

There are many useful middleware developed by the community:

// Morgan for request logging
const morgan = require('morgan');
app.use(morgan('dev'));

// Helmet to improve security
const helmet = require('helmet');
app.use(helmet());

// CORS to handle cross-origin requests
const cors = require('cors');
app.use(cors());

Writing Custom Middleware

You can write your own custom middleware:

// Authentication middleware with JWT
const jwt = require('jsonwebtoken');

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (token == null) return res.sendStatus(401);
  
  jwt.verify(token, process.env.TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something went wrong!');
});

5. Template Engines and Views

Configuring Pug/EJS/Handlebars

Express can be configured to use template engines like Pug, EJS, or Handlebars:

// Configuring EJS
app.set('view engine', 'ejs');
app.set('views', './views');

// Rendering a view
app.get('/', (req, res) => {
  res.render('index', { title: 'Homepage', message: 'Welcome' });
});

Practical Example with EJS

An example of an EJS template (views/index.ejs):

<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
</head>
<body>
  <h1><%= message %></h1>
  <ul>
    <% for(let i=0; i<articles.length; i++) { %>
      <li><%= articles[i].name %></li>
    <% } %>
  </ul>
</body>
</html>

6. Database Connection

Integration with MongoDB (Mongoose)

Mongoose is a library that simplifies interaction with MongoDB:

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/my-db', { useNewUrlParser: true, useUnifiedTopology: true });

// Defining a schema
const userSchema = new mongoose.Schema({
  name: String,
  email: String,
  password: String
});

const User = mongoose.model('User', userSchema);

// CRUD operations
app.post('/users', async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).send(user);
  } catch (error) {
    res.status(400).send(error);
  }
});

app.get('/users', async (req, res) => {
  try {
    const users = await User.find();
    res.send(users);
  } catch (error) {
    res.status(500).send(error);
  }
});

Using with SQL (Sequelize or Knex)

For SQL databases, you can use Sequelize or Knex:

// Example with Sequelize and PostgreSQL
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('database', 'username', 'password', {
  host: 'localhost',
  dialect: 'postgres'
});

// Defining a model
const User = sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true
  }
});

// Synchronizing with the database
sequelize.sync();

// CRUD operations
app.post('/users', async (req, res) => {
  try {
    const user = await User.create(req.body);
    res.status(201).send(user);
  } catch (error) {
    res.status(400).send(error);
  }
});

7. Authentication and Security

JWT (JSON Web Tokens)

JWT is a popular method for authentication in APIs:

const jwt = require('jsonwebtoken');

// Login
app.post('/login', async (req, res) => {
  // Verifying credentials
  const user = await User.findOne({ email: req.body.email });
  if (!user || !user.verifyPassword(req.body.password)) {
    return res.status(401).send('Invalid credentials');
  }
  
  // Generating the token
  const token = jwt.sign({ id: user._id }, process.env.TOKEN_SECRET, { expiresIn: '1h' });
  res.send({ token });
});

// Protected route
app.get('/profile', authenticateToken, async (req, res) => {
  const user = await User.findById(req.user.id);
  res.send(user);
});

Security Best Practices

Some security best practices:

// Protection against XSS
app.use(helmet());

// Rate limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit of 100 requests per IP
});
app.use(limiter);

// Data validation
const { body, validationResult } = require('express-validator');

app.post('/users',
  body('email').isEmail(),
  body('password').isLength({ min: 6 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Proceed with user creation
  }
);

8. Deployment and Performance

Preparation for Production

// Environment variables
require('dotenv').config();
const port = process.env.PORT || 3000;

// Advanced logging
const winston = require('winston');
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'user-service' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

Optimization

// Compression
const compression = require('compression');
app.use(compression());

// Clustering
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);
  
  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork();
  });
} else {
  // Workers can share any TCP connection
  // In this case it is an HTTP server
  const app = express();
  // ... app configuration
  app.listen(port);
  
  console.log(`Worker ${process.pid} started`);
}

Deployment on Cloud Platforms

Express can be easily deployed on platforms like Heroku, Vercel, AWS, or Railway. For example, for Heroku just add a Procfile:

web: node app.js

And then run:

heroku create
git push heroku main

9. Deep Dives and Alternatives

GraphQL with Express.js

Express can be integrated with GraphQL:

const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

const schema = buildSchema(`
  type Query {
    hello: String
  }
`);

const root = {
  hello: () => 'Hello world!'
};

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

Serverless Express.js (AWS Lambda)

Express can also be used in a serverless environment:

// handler.js
const serverless = require('serverless-http');
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello Serverless!');
});

module.exports.handler = serverless(app);

Differences with Fastify and NestJS

As I mentioned earlier, there are some key differences:

  • Fastify is faster than Express and has built-in support for JSON Schema validation.
  • NestJS is a complete framework built on top of Express (or Fastify) that uses TypeScript and a modular architecture inspired by Angular.

Conclusion

Express.js remains one of the most popular and versatile frameworks for web application development with Node.js. Its simplicity and flexibility make it suitable for both small projects and more complex applications.

It’s particularly useful if you want complete control over the structure of your application and don’t want to be limited by overly opinionated frameworks.

To learn more, you can consult the official Express.js documentation, follow tutorials on platforms like Udemy or MDN, and participate in the community on GitHub or Stack Overflow.

Express.js isn’t always the best choice for every project. If you need extreme performance, you might consider Fastify. If you prefer a more structured framework with TypeScript, NestJS might be more suitable. But for most use cases, Express offers a great balance between simplicity and power.