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.bodyafter 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