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.