Module 16: JavaScript Backend

Built-in and Third-party Middleware in Express.js

Express.js Built-in Middleware

Express.js comes with several built-in middleware functions that provide essential functionality for web applications. These middleware functions are part of the Express framework and don't require any additional installations.

Analogy: Think of built-in middleware as the standard equipment that comes with a new car—basic but essential components like headlights, windshield wipers, and a steering wheel. They provide core functionality that almost every application needs, without requiring you to shop for additional parts.

graph TD A[Express App] --> B[express.json] A --> C[express.urlencoded] A --> D[express.static] A --> E[express.raw] A --> F[express.text] A --> G[express.Router] B --> B1[Parses JSON] C --> C1[Parses URL-encoded] D --> D1[Serves static files] E --> E1[Parses raw bodies] F --> F1[Parses text bodies] G --> G1[Creates modular routes]

express.json

The express.json() middleware parses incoming requests with JSON payloads. It populates the req.body property with the parsed data.


// Setting up express.json middleware
const express = require('express');
const app = express();

// Parse JSON bodies
app.use(express.json());

// Route handler that uses parsed JSON data
app.post('/api/users', (req, res) => {
  // req.body contains the parsed JSON data
  console.log(req.body);
  
  // You can access specific properties
  const { name, email } = req.body;
  
  res.json({ 
    message: 'User created', 
    user: { name, email } 
  });
});
            

Configuration Options

The express.json() middleware accepts an options object with several configuration parameters:


app.use(express.json({
  // Size limit for the JSON payload (default: '100kb')
  limit: '1mb',
  
  // Only parse objects and arrays (default: true)
  strict: true,
  
  // Content-Type values that should be parsed as JSON (default: 'application/json')
  type: 'application/json',
  
  // Control if detailed error messages should be sent to client (default: true)
  inflate: true,
  
  // Reviver function for JSON.parse (default: null)
  reviver: null
}));
            

Real-world example: REST APIs like Twitter's or GitHub's use express.json() to parse incoming data when you create a new tweet or issue. When you post a new tweet with a JSON payload containing your message and media, this middleware extracts that data so the application can process and store it properly.

express.urlencoded

The express.urlencoded() middleware parses incoming requests with URL-encoded payloads (typically from HTML forms). Like express.json(), it populates the req.body property with the parsed data.


// Setting up express.urlencoded middleware
const express = require('express');
const app = express();

// Parse URL-encoded bodies (default form submissions)
app.use(express.urlencoded({ extended: true }));

// Route handler for a form submission
app.post('/login', (req, res) => {
  // req.body contains the parsed form data
  const { username, password } = req.body;
  
  // Process login
  if (username === 'admin' && password === 'secret') {
    res.send('Login successful');
  } else {
    res.status(401).send('Invalid credentials');
  }
});
            

The extended Option

The extended option determines which library is used to parse the URL-encoded data:


// Example of extended parsing difference

// With extended: false
// Form data: user[name]=John&user[email]=john@example.com
// req.body = { 'user[name]': 'John', 'user[email]': 'john@example.com' }

// With extended: true
// Form data: user[name]=John&user[email]=john@example.com
// req.body = { user: { name: 'John', email: 'john@example.com' } }
            

Analogy: If express.json() is like a translator who understands the structured format of JSON documents, express.urlencoded() is like a form-filler who knows how to extract information from traditional paper forms (HTML forms) and organize it into a structured format your application can understand.

express.static

The express.static() middleware serves static files such as HTML, CSS, images, and JavaScript files. It takes the directory path of the files you want to serve as its argument.


// Setting up express.static middleware
const express = require('express');
const path = require('path');
const app = express();

// Serve static files from the 'public' directory
app.use(express.static('public'));

// Files in public directory are now accessible at root URL:
// public/styles.css -> http://localhost:3000/styles.css
// public/images/logo.png -> http://localhost:3000/images/logo.png
// public/js/app.js -> http://localhost:3000/js/app.js

// You can also specify a virtual path prefix
app.use('/assets', express.static('public'));
// Now files are accessible with the /assets prefix:
// public/styles.css -> http://localhost:3000/assets/styles.css

// Using an absolute path is recommended in production
app.use(express.static(path.join(__dirname, 'public')));
            

Configuration Options

The express.static() middleware accepts a second parameter with several configuration options:


app.use(express.static('public', {
  // Set custom caching headers (default: undefined)
  maxAge: '1d',
  
  // Enable or disable etag generation (default: true)
  etag: true,
  
  // Enable or disable Last-Modified headers (default: true)
  lastModified: true,
  
  // Set proper MIME type based on file extension (default: true)
  setHeaders: (res, path, stat) => {
    // Custom header setting function
    if (path.endsWith('.pdf')) {
      res.set('Content-Disposition', 'attachment');
    }
  }
}));
            

Multiple Static Directories

You can serve files from multiple directories by calling express.static() multiple times:


// Serve static files from multiple directories
app.use(express.static('public'));
app.use(express.static('uploads'));
app.use(express.static('vendor'));

// Express looks for files in each directory in the order they are defined
// If a file exists in multiple directories, the first one takes precedence
            

Real-world example: When you visit a website like Airbnb, all the images, CSS styles, and client-side JavaScript you see and interact with are served using static file middleware. Without it, every asset would require custom route handlers, making the application much more complex to build and maintain.

Other Built-in Middleware

express.raw

The express.raw() middleware parses incoming request bodies into a Buffer and populates the req.body property. This is useful for processing binary data.


// Parse raw bodies (like binary data)
app.use(express.raw({
  type: 'application/octet-stream',
  limit: '10mb'
}));

// Route handler for binary data
app.post('/upload-binary', (req, res) => {
  // req.body is a Buffer
  console.log('Received binary data of size:', req.body.length);
  
  // Process the binary data...
  
  res.send('Binary data received');
});
            

express.text

The express.text() middleware parses incoming request bodies into a string and populates the req.body property. This is useful for text-based data formats other than JSON.


// Parse text bodies
app.use(express.text({
  type: 'text/plain',
  limit: '1mb'
}));

// Route handler for text data
app.post('/receive-text', (req, res) => {
  // req.body is a string
  console.log('Received text:', req.body);
  
  // Process the text data...
  
  res.send('Text received');
});
            

express.Router

While not technically middleware in the traditional sense, express.Router is a built-in class that creates modular route handlers. It allows you to group related routes together and apply middleware to specific route groups.


// Creating a router instance
const express = require('express');
const router = express.Router();

// Router-specific middleware
router.use((req, res, next) => {
  console.log('Router middleware:', Date.now());
  next();
});

// Define routes on the router
router.get('/', (req, res) => {
  res.send('API Home');
});

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

// Mount the router on the app
app.use('/api', router);
// Now routes are accessible as /api/ and /api/users
            
Express Built-in Middleware Building Blocks express.static HTML CSS, Images, JS Public Assets express.json Parse JSON payloads Content-Type: application/json express.urlencoded Parse form data Content-Type: application/x-www-form-urlencoded express.raw Parse binary data Content-Type: application/octet-stream express.text Parse text data Content-Type: text/plain express.Router Modular route handlers Route grouping Middleware scoping

Popular Third-party Middleware

While Express comes with essential built-in middleware, the ecosystem offers numerous third-party middleware packages for additional functionality. These packages extend Express with specialized features for common web application needs.

Analogy: Third-party middleware is like aftermarket parts and accessories for your car. While the built-in components handle basic functionality, these additional parts offer specialized features—like advanced entertainment systems, security alarms, or custom lighting—that enhance the driving experience but aren't necessary for every driver.

graph LR A[Express App] --> B[morgan] A --> C[cors] A --> D[helmet] A --> E[cookie-parser] A --> F[express-session] A --> G[compression] A --> H[multer] A --> I[passport] B -->|Logging| B1[Development] C -->|Cross-Origin| C1[Security] D -->|HTTP Headers| D1[Security] E -->|Cookies| E1[Data Handling] F -->|Sessions| F1[Authentication] G -->|Compression| G1[Performance] H -->|File Uploads| H1[Data Handling] I -->|Authentication| I1[Security]

morgan - HTTP Request Logger

Morgan is a popular HTTP request logger middleware that logs request details to the console or other output streams.


const express = require('express');
const morgan = require('morgan');
const app = express();

// Use morgan with a predefined format
app.use(morgan('dev'));
// Output: GET /home 200 6.123 ms - 1234

// Other predefined formats:
// tiny: Shows minimal output
// common: Apache common log format
// combined: Apache combined log format
// short: Shorter than default, includes response time

// Custom format
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));

// Write logs to a file
const fs = require('fs');
const path = require('path');
const accessLogStream = fs.createWriteStream(
  path.join(__dirname, 'access.log'), 
  { flags: 'a' }
);
app.use(morgan('combined', { stream: accessLogStream }));
            

Real-world example: Cloud hosting providers like Heroku use request logging to track application performance and diagnose issues. When you deploy an Express app to Heroku, adding Morgan helps you monitor traffic patterns, identify slow endpoints, and troubleshoot HTTP errors through their logging system.

cors - Cross-Origin Resource Sharing

The CORS middleware enables Cross-Origin Resource Sharing, allowing your Express APIs to be accessed from different domains.


const express = require('express');
const cors = require('cors');
const app = express();

// Enable CORS for all routes
app.use(cors());

// Configure CORS with options
app.use(cors({
  // Allowed origins (can be an array, string, or function)
  origin: 'https://example.com',
  
  // Allowed HTTP methods
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  
  // Allowed request headers
  allowedHeaders: ['Content-Type', 'Authorization'],
  
  // Headers exposed to the client
  exposedHeaders: ['X-Custom-Header'],
  
  // Allow credentials (cookies, authorization headers)
  credentials: true,
  
  // How long the results of a preflight request can be cached
  maxAge: 86400, // 24 hours
}));

// Enable CORS for specific routes only
app.get('/api/public', cors(), (req, res) => {
  res.json({ message: 'This is public API' });
});

// Dynamic origin based on environment
app.use(cors({
  origin: function(origin, callback) {
    const allowedOrigins = ['https://example.com', 'https://dev.example.com'];
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));
            

Analogy: CORS is like the security policy at a corporate building. By default, visitors (requests) from other companies (domains) aren't allowed in. The CORS middleware is the security guard who checks visitor IDs and compares them against an approved list, allowing only authorized visitors from specific companies to enter certain areas of the building.

helmet - Security Headers

Helmet helps secure Express apps by setting various HTTP headers to protect against common web vulnerabilities.


const express = require('express');
const helmet = require('helmet');
const app = express();

// Use helmet with default settings
app.use(helmet());

// This is equivalent to using all these middleware:
app.use(helmet.contentSecurityPolicy()); // Prevents XSS attacks
app.use(helmet.crossOriginEmbedderPolicy());
app.use(helmet.crossOriginOpenerPolicy());
app.use(helmet.crossOriginResourcePolicy());
app.use(helmet.dnsPrefetchControl());
app.use(helmet.expectCt());
app.use(helmet.frameguard()); // Prevents clickjacking
app.use(helmet.hidePoweredBy()); // Hides X-Powered-By header
app.use(helmet.hsts()); // HTTP Strict Transport Security
app.use(helmet.ieNoOpen());
app.use(helmet.noSniff()); // Prevents MIME sniffing
app.use(helmet.permittedCrossDomainPolicies());
app.use(helmet.referrerPolicy());
app.use(helmet.xssFilter()); // Provides basic XSS protection

// Custom configuration
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "trusted-cdn.com"],
      },
    },
    hsts: {
      maxAge: 31536000, // 1 year
      includeSubDomains: true,
      preload: true
    }
  })
);
            

Real-world example: Financial institutions like banks use Helmet to enhance the security of their web applications. It helps protect sensitive customer data by preventing various attacks such as cross-site scripting (XSS), clickjacking, and other injection-based vulnerabilities that could compromise user accounts.

cookie-parser - Parse Cookie Header

Cookie-parser parses the Cookie header and populates req.cookies with an object containing the cookies.


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

// Basic usage without signing
app.use(cookieParser());

// With a secret for signed cookies
app.use(cookieParser('my_secret_key'));

// Setting cookies
app.get('/set-cookie', (req, res) => {
  // Set a regular cookie
  res.cookie('user', 'john', { maxAge: 900000, httpOnly: true });
  
  // Set a signed cookie
  res.cookie('role', 'admin', { signed: true });
  
  res.send('Cookies set');
});

// Reading cookies
app.get('/get-cookies', (req, res) => {
  // Access regular cookies
  console.log('Cookies:', req.cookies);
  
  // Access signed cookies (tamper-proof)
  console.log('Signed Cookies:', req.signedCookies);
  
  res.json({
    cookies: req.cookies,
    signedCookies: req.signedCookies
  });
});

// Clearing cookies
app.get('/clear-cookie', (req, res) => {
  res.clearCookie('user');
  res.send('Cookie cleared');
});
            

Analogy: Cookie-parser is like a hotel receptionist who manages your room key. When you arrive (make a request), the receptionist checks if you have a key card (cookie), validates it's for the right room (parses it), and either lets you in or issues a new key (sets a new cookie) when needed.

express-session - Session Management

Express-session creates a session middleware that stores session data on the server and uses cookies to identify the session.


const express = require('express');
const session = require('express-session');
const app = express();

// Basic session setup
app.use(session({
  // Secret used to sign the session ID cookie
  secret: 'keyboard cat',
  
  // Forces the session to be saved back to the session store
  resave: false,
  
  // Forces a session that is "uninitialized" to be saved to the store
  saveUninitialized: true,
  
  // Cookie settings
  cookie: { 
    secure: process.env.NODE_ENV === 'production', // HTTPS only in production
    httpOnly: true, // Prevents client-side JS from reading the cookie
    maxAge: 1000 * 60 * 60 * 24 // 24 hours
  }
}));

// Using session to store user data
app.post('/login', (req, res) => {
  // Authenticate user (simplified example)
  const { username, password } = req.body;
  
  if (username === 'admin' && password === 'secret') {
    // Store user data in session
    req.session.user = {
      id: 1,
      username: 'admin',
      role: 'administrator'
    };
    res.redirect('/dashboard');
  } else {
    res.redirect('/login?error=true');
  }
});

// Access session data
app.get('/dashboard', (req, res) => {
  // Check if user is logged in
  if (!req.session.user) {
    return res.redirect('/login');
  }
  
  res.send(`Welcome ${req.session.user.username}!`);
});

// Logout by destroying the session
app.get('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) {
      return res.redirect('/dashboard');
    }
    res.clearCookie('connect.sid');
    res.redirect('/login');
  });
});
            

Using Session Stores

By default, Express-session uses an in-memory store (MemoryStore), which is not suitable for production. For production, you should use a dedicated session store:


// Redis session store
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

// Create Redis client
const redisClient = createClient({
  url: process.env.REDIS_URL || 'redis://localhost:6379'
});

redisClient.connect().catch(console.error);

// Configure session with Redis store
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: false,
  cookie: { secure: process.env.NODE_ENV === 'production' }
}));

// Other session stores include:
// - connect-mongodb-session: MongoDB store
// - express-mysql-session: MySQL store
// - connect-pg-simple: PostgreSQL store
            

Real-world example: E-commerce platforms like Shopify use session management to maintain shopping carts. When you add items to your cart while browsing, they're stored in your session. Even if you navigate away or refresh the page, your cart items persist because they're linked to your session ID stored in a cookie.

Other Popular Middleware

compression - Response Compression

Compression middleware compresses response bodies for all requests that traverse through the middleware.


const express = require('express');
const compression = require('compression');
const app = express();

// Use compression
app.use(compression());

// With options
app.use(compression({
  // Compression level (0-9)
  level: 6,
  
  // Minimum response size in bytes to compress
  threshold: 1024,
  
  // Don't compress responses with this Content-Type
  filter: (req, res) => {
    return /text|json|javascript|css/.test(res.getHeader('Content-Type'));
  }
}));
            

multer - File Upload Handling

Multer is a middleware for handling multipart/form-data, primarily used for file uploads.


const express = require('express');
const multer = require('multer');
const app = express();

// Basic setup with default disk storage
const upload = multer({ dest: 'uploads/' });

// Single file upload
app.post('/upload', upload.single('avatar'), (req, res) => {
  // req.file contains the uploaded file info
  console.log(req.file);
  
  // req.body contains other text fields
  console.log(req.body);
  
  res.send('File uploaded');
});

// Multiple files upload
app.post('/upload-gallery', upload.array('photos', 12), (req, res) => {
  // req.files contains the uploaded files info
  console.log(req.files);
  
  res.send(`${req.files.length} files uploaded`);
});

// Advanced configuration with storage
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function (req, file, cb) {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + '.jpg');
  }
});

const upload2 = multer({ 
  storage: storage,
  limits: {
    fileSize: 1024 * 1024 * 5, // 5MB
    files: 5
  },
  fileFilter: function (req, file, cb) {
    // Accept only image files
    if (file.mimetype.startsWith('image/')) {
      cb(null, true);
    } else {
      cb(new Error('Only image files are allowed'));
    }
  }
});
            

passport - Authentication

Passport is an authentication middleware that provides a comprehensive, modular authentication system.


const express = require('express');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const app = express();

// Initialize passport
app.use(express.session({ secret: 'keyboard cat' }));
app.use(passport.initialize());
app.use(passport.session());

// Configure local strategy
passport.use(new LocalStrategy(
  function(username, password, done) {
    // Database lookup logic (simplified example)
    User.findOne({ username: username }, function (err, user) {
      if (err) { return done(err); }
      if (!user) { return done(null, false, { message: 'Incorrect username.' }); }
      if (!user.validatePassword(password)) {
        return done(null, false, { message: 'Incorrect password.' });
      }
      return done(null, user);
    });
  }
));

// Serialize/deserialize user
passport.serializeUser(function(user, done) {
  done(null, user.id);
});

passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
});

// Login route
app.post('/login',
  passport.authenticate('local', {
    successRedirect: '/dashboard',
    failureRedirect: '/login',
    failureFlash: true
  })
);

// Protected route
app.get('/dashboard',
  ensureAuthenticated,
  function(req, res) {
    res.render('dashboard', { user: req.user });
  }
);

// Middleware to check if the user is authenticated
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
}
            

Real-world example: Social media platforms like LinkedIn use Passport.js to implement "Sign in with Google/Facebook/Twitter" functionality. It provides a consistent authentication interface while supporting multiple providers, allowing users to log in with their preferred accounts rather than creating a new one.

Middleware Selection and Composition

When building Express applications, it's important to select and combine middleware strategically to create a cohesive request processing pipeline.

Common Middleware Stack

Here's an example of a typical middleware stack for a modern Express application:


const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const compression = require('compression');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const app = express();

// Request logging
app.use(morgan('dev'));

// Security headers
app.use(helmet());

// Enable CORS
app.use(cors());

// Parse requests
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Cookie handling
app.use(cookieParser());

// Session management
app.use(session({
  secret: 'secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: { secure: process.env.NODE_ENV === 'production' }
}));

// Response compression
app.use(compression());

// Static file serving
app.use(express.static('public'));

// Application routes
app.use('/api', apiRoutes);
app.use('/auth', authRoutes);
app.use('/', webRoutes);

// Error handling (should be last)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});
            
flowchart TB subgraph "Middleware Stack Order" direction TB A[HTTP Request] --> B[morgan\nRequest Logging] B --> C[helmet\nSecurity Headers] C --> D[cors\nCross-Origin Resource Sharing] D --> E[express.json\nBody Parser] E --> F[express.urlencoded\nForm Parser] F --> G[cookieParser\nCookie Handling] G --> H[session\nSession Management] H --> I[compression\nResponse Compression] I --> J[express.static\nStatic Files] J --> K[Application Routes] K --> L[Error Handling] L --> M[HTTP Response] end

Analogy: Selecting middleware is like planning a security checkpoint at an airport. You need to decide which inspections (middleware) to include and in what order. Do you check IDs (authentication) before or after scanning baggage (parsing the request body)? The order matters and affects both security and efficiency.

Practice Activities

Activity 1: Request Logging and Analysis

Create an Express application that uses Morgan for request logging:

  • Configure Morgan with different predefined formats (dev, combined, etc.)
  • Create a custom logging format that includes the request body
  • Set up log rotation with a file stream
  • Add middleware to track response times

Generate different types of requests and analyze the logs.

Activity 2: User Authentication System

Implement a complete user authentication system using:

  • express-session for session management
  • cookie-parser for handling cookies
  • bcrypt for password hashing (npm install bcrypt)
  • Custom middleware for role-based access control

Create routes for registration, login, logout, and protected content.

Activity 3: Secure API with File Upload

Build a secure REST API that includes:

  • Helmet for securing HTTP headers
  • CORS configuration for specific origins
  • Rate limiting middleware (express-rate-limit)
  • Multer for handling file uploads
  • Validation middleware for request bodies

Test the API's security with tools like Postman or curl.

Key Takeaways

In our next lecture, we'll explore creating custom middleware to add specialized functionality to your Express applications.