NODE.JS WEB FRAMEWORKS

Introduction to Express.js

Fast, Unopinionated, Minimalist Web Framework for Node.js
19 slides · routing, middleware, error handling, security, production · 2026
01

Agenda

Foundations

  • What Is Express.js?
  • Hello World & App Structure
  • Routing — Methods, Paths, Parameters
  • Route Organisation
  • Request & Response Objects

Middleware & Built-Ins

  • Middleware Fundamentals
  • Built-in Middleware
  • Third-Party Middleware
  • Error Handling

Integration & Configuration

  • Template Engine Integration
  • Static File Serving
  • Environment & Configuration
  • Security Best Practices

Production

  • Project Structure & Patterns
  • Express 5 — What's New
  • Complete Working Example
  • Summary & Next Steps
02

What Is Express.js?

History & Philosophy

Express was created by TJ Holowaychuk in 2010, inspired by Ruby's Sinatra. It is the de facto standard web framework for Node.js — minimalist by design, providing a thin layer of features without obscuring Node's own APIs.

Key Characteristics

  • Minimalist core — routing, middleware, and HTTP utilities only
  • Unopinionated — no prescribed project structure or ORM
  • Middleware-driven — everything is a function in the request pipeline
  • ~35M weekly npm downloads — most popular Node.js framework
  • Massive ecosystem — thousands of middleware packages available

Install & Quick Start

mkdir my-app && cd my-app
npm init -y
npm install express
const express = require('express');
const app = express();

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

app.listen(3000, () => {
  console.log('Server running on :3000');
});

Built On Node's http Module

Express wraps Node's native http.createServer(), adding routing, middleware chaining, and a cleaner API — but you can still access the raw req and res objects underneath.

03

Hello World & App Structure

Creating the App

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

// app is both a function and an object
// — it holds settings, routes, and
//   acts as the request handler

Defining Routes

// HTTP method + path + handler
app.get('/', (req, res) => {
  res.send('Homepage');
});

app.get('/about', (req, res) => {
  res.send('About page');
});

app.post('/contact', (req, res) => {
  res.json({ received: true });
});

Starting the Server

const PORT = process.env.PORT || 3000;

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

// Returns an http.Server instance
// — you can use it for WebSockets, etc.

Request Lifecycle

Incoming Request Middleware Stack Route Handler Response Sent

app.set() & app.get()

// Store application settings
app.set('view engine', 'ejs');
app.set('port', 3000);

// Retrieve settings
app.get('view engine'); // 'ejs'
app.get('env');         // 'development'

// Note: app.get(path, handler) is
// also the route method — Express
// overloads based on argument count
04

Routing — Methods, Paths, Parameters

HTTP Methods

app.get('/users',    handler); // Read
app.post('/users',   handler); // Create
app.put('/users/1',  handler); // Replace
app.patch('/users/1',handler); // Update
app.delete('/users/1',handler);// Delete

// Match ALL methods
app.all('/secret', requireAuth, handler);

Route Parameters

// Named parameters — captured in req.params
app.get('/users/:id', (req, res) => {
  console.log(req.params.id); // '42'
  res.send(`User ${req.params.id}`);
});

// Multiple params
app.get('/users/:userId/posts/:postId',
  (req, res) => {
    // req.params = { userId, postId }
  }
);

Query Strings

// GET /search?q=express&page=2
app.get('/search', (req, res) => {
  req.query.q;    // 'express'
  req.query.page; // '2' (always string)
});

Pattern Matching

// Regex in route paths
app.get(/.*fly$/, handler);
// matches butterfly, dragonfly

// Optional parameter (Express 5 style)
app.get('/users/:id?', handler);

// Route chaining with app.route()
app.route('/books')
  .get((req, res)  => { /* list */ })
  .post((req, res) => { /* create */ });

Multiple Handlers

// Array of handlers / middleware chain
app.get('/dashboard',
  authenticate,
  authorise('admin'),
  (req, res) => {
    res.render('dashboard');
  }
);

// Each handler can call next()
// to pass control to the next one
05

Route Organisation

express.Router()

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

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

router.get('/:id', (req, res) => {
  res.json(users[req.params.id]);
});

router.post('/', (req, res) => {
  // create user
});

module.exports = router;

Mounting Routers

// app.js
const usersRouter = require('./routes/users');
const postsRouter = require('./routes/posts');

app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);

// GET /api/users     → router.get('/')
// GET /api/users/42  → router.get('/:id')
// GET /api/posts     → postsRouter.get('/')

Router-Level Middleware

const adminRouter = express.Router();

// Applies only to this router's routes
adminRouter.use(requireAuth);
adminRouter.use(requireAdmin);

adminRouter.get('/dashboard', handler);
adminRouter.get('/settings', handler);

app.use('/admin', adminRouter);

Modular Route Files

routes/
├── index.js      ← loads all routers
├── auth.js       ← /auth/*
├── users.js      ← /api/users/*
├── posts.js      ← /api/posts/*
└── admin.js      ← /admin/*

router.param()

// Pre-process :id for all routes
router.param('id', async (req, res, next, id) => {
  req.user = await User.findById(id);
  if (!req.user) return res.status(404).json(
    { error: 'Not found' }
  );
  next();
});
06

Request & Response Objects

req — Key Properties

PropertyDescription
req.paramsRoute parameters (:id)
req.queryQuery string (?q=x)
req.bodyParsed body (needs middleware)
req.headersRequest headers object
req.methodGET, POST, PUT, etc.
req.pathURL path without query
req.cookiesCookies (needs cookie-parser)
req.ipClient IP address

req.body Example

// Requires body-parsing middleware
app.use(express.json());
app.use(express.urlencoded({
  extended: true
}));

app.post('/api/users', (req, res) => {
  console.log(req.body.name);
  console.log(req.body.email);
});

res — Key Methods

MethodPurpose
res.send()Send string/Buffer/object
res.json()Send JSON response
res.render()Render a view template
res.redirect()Redirect to another URL
res.status()Set HTTP status code
res.sendFile()Send a file as response
res.set()Set response headers
res.cookie()Set a cookie

Chaining & Common Patterns

// Status + JSON
res.status(201).json({ id: newUser.id });

// Status + Render
res.status(404).render('404', {
  title: 'Not Found'
});

// Redirect with status
res.redirect(301, '/new-url');

// Set header + send
res.set('X-Request-Id', uuid()).json(data);
07

Middleware Fundamentals

The Middleware Signature

// Every middleware is a function with
// (req, res, next)
const logger = (req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next(); // pass to next middleware
};

app.use(logger);

If you don't call next(), the request hangs — no response is ever sent.

Order Matters

// Middleware executes top to bottom
app.use(express.json());     // 1st
app.use(cors());             // 2nd
app.use(helmet());           // 3rd
app.use('/api', authMiddleware); // 4th

app.get('/api/data', handler); // 5th

// Error handler must be LAST
app.use(errorHandler);        // 6th

Types of Middleware

TypeScope
Application-levelapp.use(fn) — runs on every request
Router-levelrouter.use(fn) — scoped to router
Route-levelInline: app.get(path, fn, handler)
Error-handling(err, req, res, next) — 4 args
Built-inexpress.json(), express.static()
Third-partycors(), helmet(), morgan()

Middleware Pipeline

json() cors() auth() route handler next() next() next()

Each middleware can modify req/res, end the cycle, or call next() to continue.

08

Built-in Middleware

express.json()

// Parses JSON request bodies
app.use(express.json());

// Options
app.use(express.json({
  limit: '10kb',      // max body size
  strict: true,       // only objects/arrays
  type: 'application/json'
}));

// POST { "name": "Ada" }
app.post('/api/users', (req, res) => {
  console.log(req.body.name); // 'Ada'
});

express.urlencoded()

// Parses URL-encoded form data
app.use(express.urlencoded({
  extended: true   // use qs library
}));

// <form method="POST">
//   <input name="email" value="a@b.com">
// </form>
app.post('/login', (req, res) => {
  console.log(req.body.email); // 'a@b.com'
});

express.static()

const path = require('path');

// Serve files from /public
app.use(express.static(
  path.join(__dirname, 'public')
));

// With virtual path prefix
app.use('/assets', express.static(
  path.join(__dirname, 'public')
));
// /assets/css/style.css → public/css/style.css

// Options
app.use(express.static('public', {
  maxAge: '1d',        // cache control
  index: 'index.html', // default file
  dotfiles: 'ignore'   // hide .files
}));

express.raw() & express.text()

// Parse body as Buffer
app.use('/webhook',
  express.raw({ type: 'application/octet-stream' })
);

// Parse body as plain text
app.use('/logs',
  express.text({ type: 'text/plain' })
);

Less common but useful for webhooks (Stripe, GitHub) and log ingestion endpoints.

09

Third-Party Middleware

Essential Packages

PackagePurpose
corsCross-Origin Resource Sharing headers
helmetSecurity headers (15+ protections)
morganHTTP request logging
compressionGzip/Brotli response compression
express-rate-limitRate limiting per IP/key
cookie-parserParse cookies into req.cookies
express-sessionServer-side session management
multerMultipart/file upload handling

Setup Example

const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const rateLimit = require(
  'express-rate-limit'
);

app.use(helmet());
app.use(cors({ origin: 'https://mysite.com' }));
app.use(compression());
app.use(morgan('combined'));

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 min
  max: 100  // 100 requests per window
});
app.use('/api', limiter);

Tip: Order Convention

Security first (helmet, cors), then parsing (json, urlencoded), then logging (morgan), then routes, then error handlers last.

10

Error Handling

Error Middleware Signature

// MUST have exactly 4 parameters
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message
  });
});

// Trigger with next(err)
app.get('/fail', (req, res, next) => {
  const err = new Error('Something broke');
  err.status = 500;
  next(err); // skips to error handler
});

Custom Error Classes

class AppError extends Error {
  constructor(message, status = 500) {
    super(message);
    this.status = status;
    this.isOperational = true;
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, 404);
  }
}

// Usage in route
app.get('/users/:id', async (req, res, next) => {
  const user = await User.findById(req.params.id);
  if (!user) return next(new NotFoundError('User'));
  res.json(user);
});

Async Error Catching

// Express 4: wrap async handlers
const asyncHandler = (fn) =>
  (req, res, next) =>
    Promise.resolve(fn(req, res, next))
      .catch(next);

app.get('/users', asyncHandler(
  async (req, res) => {
    const users = await User.find();
    res.json(users);
    // Thrown errors auto-forwarded to
    // error middleware via .catch(next)
  }
));

// Express 5: automatic — no wrapper needed!
// Rejected promises are caught automatically

404 Handler

// Place AFTER all routes, BEFORE error handler
app.use((req, res, next) => {
  res.status(404).json({
    error: `Cannot ${req.method} ${req.path}`
  });
});

Tip: Layered Error Handling

Use operational errors (bad input, not found) for client-facing messages and programmer errors (bugs) for logging + generic 500. Never leak stack traces in production.

11

Template Engine Integration

Setting Up a View Engine

const path = require('path');

// Tell Express which engine to use
app.set('view engine', 'ejs');

// Tell Express where views live
app.set('views', path.join(
  __dirname, 'views'
));

// Now .ejs extension is optional:
res.render('home'); // views/home.ejs

res.render() in Action

app.get('/', (req, res) => {
  res.render('home', {
    title: 'Welcome',
    user: req.user,
    items: [1, 2, 3]
  });
});

// Data available in the template:
// <%= title %>  <%= user.name %>

app.locals & res.locals

// Global — available in every render
app.locals.siteName = 'My App';

// Per-request — set in middleware
app.use((req, res, next) => {
  res.locals.currentUser = req.user;
  next();
});

Popular Engines

EngineSyntaxInstall
EJS<%= %> JS in HTMLnpm i ejs
PugIndentation-basednpm i pug
Handlebars{{ }} Mustachenpm i hbs
Nunjucks{% %} Jinja2npm i nunjucks

Using Multiple Engines

// Register custom engines
const ejs = require('ejs');
const pug = require('pug');

app.engine('ejs', ejs.renderFile);
app.engine('pug', pug.renderFile);

// Render with explicit extension
res.render('email.ejs', data);
res.render('page.pug', data);

// Default engine for extensionless files
app.set('view engine', 'ejs');
12

Static File Serving

express.static() Basics

const path = require('path');

// Serve from /public directory
app.use(express.static(
  path.join(__dirname, 'public')
));

// File structure:
// public/
//   css/style.css    → /css/style.css
//   js/app.js        → /js/app.js
//   images/logo.png  → /images/logo.png
//   index.html       → / (auto-served)

Virtual Path Prefix

// Files accessible under /static/*
app.use('/static', express.static(
  path.join(__dirname, 'public')
));

// public/css/style.css
//   → /static/css/style.css

// Multiple static directories
app.use(express.static('public'));
app.use(express.static('uploads'));
// Express searches in order declared

Caching & Performance Headers

app.use(express.static('public', {
  maxAge: '30d',          // Cache-Control
  etag: true,             // ETag header
  lastModified: true,     // Last-Modified
  immutable: true,        // immutable flag
  index: false,           // disable auto index
  dotfiles: 'ignore',     // hide .dotfiles
  redirect: false,        // no trailing / redirect
  fallthrough: true       // pass to next handler
}));

// Versioned assets for cache-busting
// /assets/style.abc123.css → maxAge: '1y'

Production Tips

  • Use a reverse proxy (Nginx, Cloudflare) for static files in production
  • Set long maxAge for versioned/hashed assets
  • Use compression middleware for dynamic responses
  • Serve static before session/auth middleware to avoid unnecessary processing
  • Consider a CDN for global distribution
13

Environment & Configuration

NODE_ENV

// Express checks NODE_ENV internally
// 'production' enables:
//   - view caching
//   - less verbose errors
//   - better performance

// Check in code
if (app.get('env') === 'production') {
  app.set('trust proxy', 1);
  app.use(helmet());
}

// Set via CLI
// NODE_ENV=production node app.js

dotenv for Config

// npm install dotenv
require('dotenv').config();

// .env file
// PORT=3000
// DB_URL=mongodb://localhost/mydb
// SESSION_SECRET=keyboard_cat
// API_KEY=sk_live_abc123

const PORT = process.env.PORT || 3000;
const DB = process.env.DB_URL;

// NEVER commit .env to git!
// Add to .gitignore

app.set() / app.get()

SettingPurpose
envEnvironment mode
view engineTemplate engine
viewsViews directory
view cacheCache compiled templates
trust proxyTrust X-Forwarded-* headers
json spacesPrettify JSON output
etagETag response header
case sensitive routing/Foo ≠ /foo

trust proxy

// Behind Nginx, AWS ELB, Heroku, etc.
app.set('trust proxy', 1);

// Enables correct:
// - req.ip (client IP, not proxy IP)
// - req.protocol ('https' not 'http')
// - req.hostname
// - secure cookies over HTTPS

Important: Only enable when behind a trusted proxy. Otherwise clients can spoof headers.

14

Security Best Practices

Helmet — Security Headers

const helmet = require('helmet');
app.use(helmet());

// Sets 15+ HTTP headers:
// - Content-Security-Policy
// - X-Content-Type-Options: nosniff
// - X-Frame-Options: DENY
// - Strict-Transport-Security
// - X-XSS-Protection
// ...and more

// Customise individual headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "cdn.example.com"]
    }
  }
}));

CORS Configuration

const cors = require('cors');

// Allow specific origin
app.use(cors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST'],
  credentials: true
}));

// Dynamic origin
app.use(cors({
  origin: (origin, cb) => {
    const allowed = ['https://a.com', 'https://b.com'];
    cb(null, allowed.includes(origin));
  }
}));

Rate Limiting

const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: { error: 'Too many requests' },
  standardHeaders: true,
  legacyHeaders: false
});

app.use('/api/', apiLimiter);

// Stricter for auth routes
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 5
});
app.use('/auth/login', authLimiter);

Input Validation & CSRF

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

app.post('/users',
  body('email').isEmail().normalizeEmail(),
  body('name').trim().notEmpty().escape(),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty())
      return res.status(400).json(
        { errors: errors.array() }
      );
  }
);

Always validate and sanitise input. Never trust req.body, req.params, or req.query directly.

15

Project Structure & Patterns

MVC Directory Layout

my-express-app/
├── app.js              ← Express setup
├── server.js           ← listen() entry
├── package.json
├── .env
├── config/
│   ├── db.js
│   └── index.js        ← merged config
├── routes/
│   ├── index.js        ← mounts all routers
│   ├── auth.js
│   ├── users.js
│   └── posts.js
├── controllers/
│   ├── authController.js
│   ├── userController.js
│   └── postController.js
├── services/
│   ├── userService.js
│   └── emailService.js
├── models/
│   ├── User.js
│   └── Post.js
├── middleware/
│   ├── auth.js
│   ├── validate.js
│   └── errorHandler.js
├── views/
│   ├── layouts/
│   ├── partials/
│   └── pages/
├── public/
│   ├── css/
│   ├── js/
│   └── images/
└── tests/
    ├── routes/
    └── services/

Separation of Concerns

  • app.js — Express config, middleware, mount routes
  • server.js — app.listen() only (testable app)
  • routes/ — URL mapping, input validation
  • controllers/ — handle req/res, call services
  • services/ — business logic, DB queries
  • models/ — data schemas (Mongoose, Sequelize)
  • middleware/ — reusable req/res transformations

Route → Controller → Service

// routes/users.js
router.get('/:id', userController.getById);

// controllers/userController.js
exports.getById = async (req, res, next) => {
  try {
    const user = await userService
      .findById(req.params.id);
    res.json(user);
  } catch (err) { next(err); }
};

// services/userService.js
exports.findById = (id) =>
  User.findById(id).lean();

Tip: Separate app from server

Export app from app.js and import it in server.js and in tests. This lets you run supertest(app) without starting a real server.

16

Express 5 — What's New

Automatic Promise Rejection Handling

// Express 4 — must wrap or use library
app.get('/data', async (req, res, next) => {
  try {
    const data = await fetchData();
    res.json(data);
  } catch (err) {
    next(err); // manual forwarding
  }
});

// Express 5 — just throw!
app.get('/data', async (req, res) => {
  const data = await fetchData();
  res.json(data);
  // Rejected promise → error middleware
  // automatically — no wrapper needed
});

Stricter Path Matching

// Express 4: loose, regex-like
app.get('/ab?cd', handler);
// matches /acd, /abcd

// Express 5: uses path-to-regexp v8
// — No optional characters with ?
// — Use {param} for optional segments
app.get('/users{/:id}', handler);
// matches /users and /users/42

// Wildcards use * syntax
app.get('/files/*path', handler);
// req.params.path = rest of URL

Removed / Changed APIs

Express 4Express 5
app.del()Removed — use app.delete()
req.hostRemoved — use req.hostname
req.param()Removed — use req.params / req.query
res.json(status, obj)Removed — chain .status().json()
res.send(status)Removed — use res.sendStatus()

Other Improvements

  • req.query now returns a getter (re-parsed on each access)
  • Brotli encoding support for res.sendFile()
  • res.render() returns a Promise when no callback given
  • Node 18+ required as minimum version

Install Express 5

npm install express@5

Express 5 reached stable release in 2024 after years in alpha/beta. Migration is straightforward for most apps — the biggest change is path matching syntax.

17

Complete Working Example

app.js — REST API + Views

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

// Config
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Middleware
app.use(helmet());
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));

app.locals.siteName = 'TaskBoard';
app.locals.year = new Date().getFullYear();

let tasks = [
  { id: 1, title: 'Learn Express',
    done: false },
  { id: 2, title: 'Build an API',
    done: true },
];
let nextId = 3;

// Views
app.get('/', (req, res) => {
  res.render('home', { title: 'Tasks', tasks });
});

// REST API
app.get('/api/tasks', (req, res) => {
  res.json(tasks);
});

app.post('/api/tasks', (req, res) => {
  const { title } = req.body;
  if (!title) return res.status(400)
    .json({ error: 'Title required' });
  const task = { id: nextId++, title, done: false };
  tasks.push(task);
  res.status(201).json(task);
});

app.patch('/api/tasks/:id', (req, res) => {
  const task = tasks.find(
    t => t.id === +req.params.id
  );
  if (!task) return res.status(404)
    .json({ error: 'Not found' });
  Object.assign(task, req.body);
  res.json(task);
});

app.delete('/api/tasks/:id', (req, res) => {
  tasks = tasks.filter(
    t => t.id !== +req.params.id
  );
  res.sendStatus(204);
});

Error Handling & Start

// 404 handler
app.use((req, res) => {
  res.status(404).render('404', {
    title: 'Not Found'
  });
});

// Error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    error: 'Internal Server Error'
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`TaskBoard running on :${PORT}`);
});

views/home.ejs

<h1>Task List</h1>
<ul>
  <% tasks.forEach(t => { %>
    <li class="<%= t.done ? 'done' : '' %>">
      <%= t.title %>
    </li>
  <% }) %>
</ul>
<form method="POST" action="/api/tasks">
  <input name="title" placeholder="New task">
  <button type="submit">Add</button>
</form>

Test with cURL

# List tasks
curl localhost:3000/api/tasks

# Create task
curl -X POST localhost:3000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Deploy app"}'

# Toggle done
curl -X PATCH localhost:3000/api/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"done":true}'

# Delete task
curl -X DELETE localhost:3000/api/tasks/2
18

Summary & Next Steps

Core Takeaways

  • Express = minimal, middleware-driven web framework for Node.js
  • Routing maps HTTP methods + paths to handler functions
  • Middleware is the core pattern — everything flows through the pipeline
  • Built-in parsers for JSON, URL-encoded, and static files
  • Error handling uses the 4-argument (err, req, res, next) signature

Best Practices

  • Use express.Router() for modular route files
  • Apply security middleware (helmet, cors, rate-limit)
  • Always validate and sanitise user input
  • Separate app.js from server.js for testability
  • Use environment variables for config (dotenv)
  • Handle errors centrally with custom error classes

Next Steps

  • Build a full CRUD REST API with Express
  • Add a database (MongoDB + Mongoose or PostgreSQL + Sequelize)
  • Implement authentication (Passport.js, JWT)
  • Add WebSocket support (Socket.IO)
  • Deploy to production (PM2, Docker, Railway)
  • Explore Express 5 path matching and async handling

Essential Resources

  • Official Docs: expressjs.com
  • GitHub: github.com/expressjs/express
  • npm: npmjs.com/package/express
  • MDN Express Tutorial: developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs

Key Packages

PackagePurpose
expressWeb framework
helmetSecurity headers
corsCross-origin resource sharing
morganHTTP request logging
express-validatorInput validation