Skip to content

ExpressJS - Cheatsheet

In this cheatsheet, we will cover the essential features and concepts of Express.js for building robust and scalable applications in 2024. We’ll explore routing, middleware, modern asynchronous patterns, and data handling. You’ll learn how to set up a project with a modern toolchain, handle various HTTP methods, manage static assets, and integrate with databases using best practices. This guide is your quick reference for building efficient APIs and web applications with Express.js.

  • Introduction
  • Modern Project Setup
  • Core Concepts: Routing
  • Core Concepts: Middleware
  • Serving Content: Views & Static Files
  • The Request and Response Objects
  • Modular Routing with express.Router
  • Application Methods
  • Modern Error Handling
  • Debugging and Logging
  • Database Integration with Async/Await
  • Secure Authentication with JWT
  • Essential Security Practices
  • Testing with Modern Tooling
  • Deployment to the Cloud

Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. It acts as a thin layer of fundamental web application features, without obscuring Node.js features that you know and love.

Create a new directory for your project, navigate into it, and initialize a package.json file. This file will track your project’s metadata and dependencies.

$ mkdir my-modern-app
$ cd my-modern-app
$ npm init -y

Install Express and add it to your project’s dependencies.

$ npm install express

This code sets up a basic Express server using modern ES Module syntax. It listens on a port defined by an environment variable (a best practice) and responds with a JSON message. To use ES Module syntax (import/export), add "type": "module" to your package.json.

import express from 'express';
// Best practice: use environment variables for configuration
const PORT = process.env.PORT || 3000;
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello Modern World!' });
});
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});

For development, it’s highly recommended to use a tool like nodemon that automatically restarts your server on file changes.

# Install nodemon as a development dependency
$ npm install --save-dev nodemon
# Add a script to your package.json
# "scripts": {
# "dev": "nodemon index.js"
# }
# Run the dev server
$ npm run dev

Routing defines how the application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, etc.).

Each route can have one or more handler functions, which are executed when the route is matched.

// GET method route
app.get('/', (req, res) => res.send('GET request to the homepage'));
// POST method route
app.post('/users', (req, res) => res.send('POST request to create a user'));
// Dynamic route with a parameter
app.get('/users/:userId', (req, res) => {
res.send(`Details for user ${req.params.userId}`);
});

You can provide multiple callback functions that behave like middleware to handle a request. The only exception is that these callbacks may invoke next('route') to bypass the remaining route callbacks.

const logTimestamp = (req, res, next) => {
console.log('Time: ', Date.now());
next();
};
app.get('/with-handler', [logTimestamp], (req, res) => {
res.send('Check the console for a timestamp!');
});

Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle. They can execute any code, make changes to the request and the response objects, end the request-response cycle, and call the next middleware in the stack.

Express has built-in middleware for common tasks. Since Express 4.16+, the popular body-parser is included.

// Middleware to parse JSON bodies
app.use(express.json());
// Middleware to parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));
// Middleware to serve static files from the 'public' directory
app.use(express.static('public'));

Writing your own middleware is simple. Here is a logger that logs the request method and path.

const requestLogger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
next(); // Pass control to the next handler
};
app.use(requestLogger);

While many modern Express apps serve as a JSON API for a frontend client (like React or Vue), it can also render dynamic HTML on the server using template engines.

Pug, EJS, and Handlebars are popular choices. Here’s how to set up EJS.

$ npm install ejs
// In your app.js
app.set('view engine', 'ejs');
// Optional: specify the directory where templates are located
// app.set('views', path.join(__dirname, 'views'));

2. Create a Template (e.g., views/profile.ejs)

Section titled “2. Create a Template (e.g., views/profile.ejs)”

Templates can embed dynamic data passed from your route handler.

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1>Hello, <%= user.name %>!</h1>
</body>
</html>

Use res.render() to render a view and send the rendered HTML string to the client.

app.get('/profile', (req, res) => {
const userData = { name: 'Alex' };
res.render('profile', {
title: 'User Profile',
user: userData
});
});

Route handlers and middleware get access to req and res objects, which represent the HTTP request and response.

PropertyExplanation
req.paramsRoute parameters (e.g., /users/:id)
req.queryQuery string parameters (e.g., ?sort=desc)
req.bodyParsed request body (requires express.json() or express.urlencoded())
req.headersIncoming request headers
req.methodHTTP method of the request (GET, POST, etc.)
req.originalUrlFull original URL of the request
MethodExplanation
res.send()Send a response of various types (String, Buffer, object)
res.json()Send a JSON response (automatically sets Content-Type)
res.status()Set the HTTP status code for the response
res.render()Render a view template
res.redirect()Redirect to a specified URL
res.cookie()Set a cookie
app.get('/api/health', (req, res) => {
res.status(200).json({ status: 'UP' });
});

For larger applications, it’s best practice to use express.Router to group route handlers in separate files, keeping your main application file clean.

1. Create a Router File (e.g., routes/userRoutes.js)

Section titled “1. Create a Router File (e.g., routes/userRoutes.js)”
import { Router } from 'express';
const router = Router();
// This route is now effectively /api/users/
router.get('/', (req, res) => {
res.send('List of all users');
});
// This route is now effectively /api/users/profile
router.get('/profile', (req, res) => {
res.send('User profile page');
});
export default router;
import express from 'express';
import userRoutes from './routes/userRoutes.js';
const app = express();
// Mount the router on the /api/users path
app.use('/api/users', userRoutes);
// ... other app setup ...
app.listen(3000);
MethodExplanation
app.use([path], callback)Mount middleware functions at a specified path
app.get(path, callback)Handle GET requests
app.post(path, callback)Handle POST requests
app.put(path, callback)Handle PUT requests
app.delete(path, callback)Handle DELETE requests
app.listen(port, [callback])Bind the application to a port and start the server
app.set(name, value)Assigns setting name to value
app.engine(ext, callback)Register the given template engine callback as ext

A centralized error-handling middleware is a best practice. It should be defined last, after all other app.use() and routes calls. This special middleware has four arguments (err, req, res, next).

// 404 handler for unmatched routes
app.use((req, res, next) => {
res.status(404).send('Sorry, that route does not exist.');
});
// Centralized error handler
app.use((err, req, res, next) => {
console.error(err.stack);
// Avoid leaking stack trace in production
const statusCode = err.statusCode || 500;
const message = process.env.NODE_ENV === 'production' ? 'Something broke!' : err.message;
res.status(statusCode).json({ error: message });
});

Effective debugging and logging are crucial for development and production.

Run your app with the --inspect flag to enable debugging with tools like Chrome DevTools or the VS Code debugger. This allows you to set breakpoints and inspect variables in real-time.

$ node --inspect index.js

For production, simple console.log is not enough. Use a structured logging library like pino or winston for filterable, machine-readable logs.

$ npm install pino
// Example usage
import pino from 'pino';
const logger = pino({ level: 'info' });
logger.info('Server started successfully.');
logger.error({ err }, 'An error occurred during request processing.');

Modern Node.js heavily favors async/await for handling asynchronous operations like database queries. This makes code cleaner and easier to reason about than callbacks or .then() chains.

The pg library is the standard for connecting to PostgreSQL. It fully supports Promises, making it perfect for async/await.

$ npm install pg
// db.js
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({
connectionString: process.env.DATABASE_URL, // e.g., postgresql://user:password@host:port/database
});
export default pool;
// usage in a route
import pool from './db.js';
app.get('/users', async (req, res, next) => {
try {
const { rows } = await pool.query('SELECT * FROM users');
res.json(rows);
} catch (err) {
next(err); // Pass error to central handler
}
});

The native mongodb driver is fully async-first.

$ npm install mongodb
// mongo.js
import { MongoClient } from 'mongodb';
const uri = process.env.MONGO_URI; // e.g., mongodb://localhost:27017
const client = new MongoClient(uri);
let db;
async function connectToDatabase() {
try {
await client.connect();
db = client.db('my-app-db');
console.log('Connected to MongoDB');
} catch (err) {
console.error('Failed to connect to MongoDB', err);
process.exit(1);
}
}
export { connectToDatabase, db };
// usage in a route
import { db } from './mongo.js';
app.get('/products', async (req, res, next) => {
try {
const products = await db.collection('products').find({}).toArray();
res.json(products);
} catch (err) {
next(err);
}
});

JSON Web Tokens (JWT) are a standard way to handle stateless authentication in APIs.

# JWT for token creation, bcrypt for hashing passwords
$ npm install jsonwebtoken bcryptjs

2. User Registration and Login (Controller Logic)

Section titled “2. User Registration and Login (Controller Logic)”

This logic uses async/await for clean, readable code and try/catch for error handling. Crucially, secrets like JWT_SECRET must be stored in environment variables, not hardcoded.

import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
// Assume User model from a database ORM like Mongoose or Prisma
import User from '../models/User.js';
export const register = async (req, res, next) => {
try {
const { email, password, name } = req.body;
const hashedPassword = await bcrypt.hash(password, 12);
const newUser = new User({ email, name, password: hashedPassword });
await newUser.save();
res.status(201).json({ message: 'User created!' });
} catch (err) {
next(err);
}
};
export const login = async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ message: 'Authentication failed' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: 'Authentication failed' });
}
const token = jwt.sign(
{ id: user._id, email: user.email },
process.env.JWT_SECRET, // Use environment variable!
{ expiresIn: '1h' }
);
res.status(200).json({ token });
} catch (err) {
next(err);
}
};

This middleware protects routes by verifying the JWT from the Authorization header.

import jwt from 'jsonwebtoken';
export const protect = (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'No token, authorization denied' });
}
const token = authHeader.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Add decoded user payload to the request
next();
} catch (err) {
res.status(401).json({ message: 'Token is not valid' });
}
};
// Usage:
// app.get('/api/protected-route', protect, (req, res) => { ... });

Security is not an afterthought. Start with these essential middleware and practices.

helmet sets various HTTP headers to help protect your app from well-known web vulnerabilities.

$ npm install helmet
// In app.js
import helmet from 'helmet';
app.use(helmet());

Use express-rate-limit to prevent brute-force attacks and denial-of-service.

$ npm install express-rate-limit
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply the rate limiting middleware to all requests
app.use(limiter);

Cross-Site Request Forgery (CSRF) is a concern for traditional web apps using cookies/sessions. For stateless APIs using JWT in headers, CSRF is less of a risk. If you are building a server-rendered app with sessions, a library like csurf’s successor or a pattern like double-submit cookies is necessary. For modern SPAs, ensure you are using SameSite cookies and proper CORS policies.

A combination of a test runner (like Jest or Vitest), and an HTTP assertion library (Supertest) is a powerful setup for testing Express applications.

We’ll use Vitest for its speed and modern features, and Supertest for making requests to our app.

$ npm install --save-dev vitest supertest

Separate your app creation from the server listening part. This allows you to import the app object into your tests without starting the server.

app.js
import express from 'express';
const app = express();
app.get('/', (req, res) => res.status(200).send('OK'));
export default app;
// index.js (or server.js)
import app from './app.js';
const PORT = 3000;
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));

3. Write an Integration Test (app.test.js)

Section titled “3. Write an Integration Test (app.test.js)”

This test uses supertest to make a real HTTP request to the in-memory app and vitest to define the test suite and assertions.

import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from '../app.js'; // Import the express app
describe('GET /', () => {
it('should respond with a 200 status code and OK text', async () => {
const response = await request(app).get('/');
expect(response.statusCode).toBe(200);
expect(response.text).toBe('OK');
});
});
describe('GET /non-existent-route', () => {
it('should respond with a 404 status code', async () => {
const response = await request(app).get('/not-a-real-route');
expect(response.statusCode).toBe(404);
});
});

Platforms like Vercel, Render, and Heroku make deploying Node.js applications straightforward. Here’s a quick guide for Vercel, which excels at serverless deployments.

Vercel can automatically convert your Express app into a serverless function. Place your main Express app file inside an /api directory.

my-modern-app/
├── api/
│ └── index.js <-- Your Express app code goes here
├── public/ <-- Static files (optional)
└── package.json

Ensure your app exports the app object. Vercel’s build process will handle the server creation.

import express from 'express';
const app = express();
app.get('/api/hello', (req, res) => {
res.send('Hello from Express on Vercel!');
});
// Export the app
export default app;

This configuration file tells Vercel to direct all incoming requests to your serverless Express app.

{
"version": 2,
"rewrites": [
{ "source": "/api/(.*)", "destination": "/api" }
]
}

Install the Vercel CLI, link your project, and deploy.

# Install Vercel CLI globally
$ npm install -g vercel
# Deploy from your project root
$ vercel
# To deploy to production
$ vercel --prod