Skip to content

Express Intro

First server. First route. First "holy shit it works" moment
Express wraps Node's native http module so you don't have to manually write response headers like a caveman. But don't forget - Express is still Node.js underneath. The event loop , the callback queue , the single-threaded architecture - all still there. Express just makes the ergonomics suck less

installation

Same ritual as every Node project

mkdir my-express-app && cd my-express-app
npm init -y
npm install express

One dependency. That's it. Express itself is lean. What you build around it determines your attack surface

# verify it installed
npm list express
# -> express@4.21.0 (or whatever 2026 version you got)

basic server - hello world

The "Hello World" of Express is 5 lines. Every server you ever build starts from here

const express = require('express')
const app = express()
const PORT = process.env.PORT || 3000

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

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

Save that as server.js and run node server.js

curl http://localhost:3000
# Hello World

That's it. Your first Express server is alive But this server has zero security headers , no error handling , no rate limiting , and leaks the X-Powered-By: Express header to every client who asks. We'll fix all of that later

what Express adds over raw http

Raw Node.js http server:

const http = require('http')

http.createServer((req , res) => {
  // manual routing , manual headers , manual everything
  if (req.url === '/' && req.method === 'GET') {
    res.writeHead(200 , { 'Content-Type': 'text/plain' })
    res.end('Hello World')
  } else if (req.url === '/api/data' && req.method === 'GET') {
    res.writeHead(200 , { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ data: 'something' }))
  } else {
    res.writeHead(404)
    res.end('Not Found')
  }
}).listen(3000)

Same thing in Express:

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

app.get('/' , (req , res) => res.send('Hello World'))
app.get('/api/data' , (req , res) => res.json({ data: 'something' }))
app.all('*' , (req , res) => res.status(404).send('Not Found'))

app.listen(3000)

Here's what Express gives you:

  • Routing - app.get() , app.post() , route params , query parsing - all built in
  • JSON parsing - res.json() serializes and sets Content-Type automatically
  • Error handling - middleware signature with (err , req , res , next) pattern
  • Static file serving - express.static() serves whole directories in one line
  • Middleware ecosystem - hundreds of packages that plug into the request pipeline
  • Response helpers - res.send() , res.json() , res.status() , res.redirect() , res.render()
  • Request parsing - req.query , req.params , req.body after using express.json()

routing basics

Express maps HTTP methods and paths to handler functions

app.get('/users' , (req , res) => { /* list users */ })
app.post('/users' , (req , res) => { /* create user */ })
app.put('/users/:id' , (req , res) => { /* update user */ })
app.delete('/users/:id' , (req , res) => { /* delete user */ })

Route parameters are captured with : prefix:

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

Query strings parsed automatically into req.query:

// GET /search?q=express&limit=10
app.get('/search' , (req , res) => {
  // req.query.q -> 'express'
  // req.query.limit -> '10'
  res.json(req.query)
})

security note - version leaking

Express sends X-Powered-By: Express in every response by default That's free intelligence for attackers. Turn it off:

app.disable('x-powered-by')

One line. Do it in every app. Every time

prerequisites

express_00_home.md - you should understand what Express is and why it dominates


next → express_02_get_started.md