Introduction to Express.js: Your First Web Server

Introduction to Express.js: Your First Web Server
You have learned Node.js fundamentals — the runtime, modules, async patterns, the file system, and npm. Now it is time to build something that communicates with the world: a web server.
Express.js is the framework that makes this possible in minutes. It is lightweight, battle-tested, and used by millions of Node.js developers. By the end of this module you will have a fully working HTTP server with multiple routes, proper response types, query parameters, route parameters, and error handling — ready to evolve into a full REST API in the modules ahead.
This is Module 9 of the Node.js Full‑Stack Developer course.
Why Express Over Raw Node.js http?
Node.js ships a built-in http module. Here is what a simple "Hello World" server looks like with it:
// ❌ Raw http — verbose and painful to scale
const http = require('http')
const server = http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ message: 'Hello World' }))
} else if (req.method === 'GET' && req.url === '/about') {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('About page')
} else {
res.writeHead(404)
res.end('Not found')
}
})
server.listen(3000, () => console.log('Listening on port 3000'))Now with Express:
// ✅ Express — clean, readable, scalable
const express = require('express')
const app = express()
app.get('/', (req, res) => res.json({ message: 'Hello World' }))
app.get('/about', (req, res) => res.send('About page'))
app.listen(3000, () => console.log('Listening on port 3000'))Same result. A fraction of the code. And as your app grows, Express scales cleanly through middleware and routers — the raw http approach becomes unmaintainable.
Setting Up Your First Express Project
mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D nodemonUpdate package.json scripts:
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
}
}Create the file structure:
my-api/
├── src/
│ └── index.js
├── package.json
└── package-lock.jsonYour First Express Server
Create src/index.js:
const express = require('express')
const app = express()
const PORT = process.env.PORT || 3000
// Parse incoming JSON request bodies
app.use(express.json())
// Parse URL-encoded form data
app.use(express.urlencoded({ extended: true }))
// ── Routes ──────────────────────────────────────────
app.get('/', (req, res) => {
res.json({ message: 'Welcome to My API', version: '1.0.0' })
})
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() })
})
// ── Start Server ─────────────────────────────────────
app.listen(PORT, () => {
console.log(`🚀 Server running at http://localhost:${PORT}`)
})Start it:
npm run devOpen your browser or use curl:
curl http://localhost:3000/
# {"message":"Welcome to My API","version":"1.0.0"}
curl http://localhost:3000/health
# {"status":"ok","uptime":12.345}The Request Object (req)
Every route handler receives req — the incoming HTTP request. Here are its most-used properties:
app.get('/demo', (req, res) => {
console.log(req.method) // 'GET'
console.log(req.url) // '/demo?name=alice'
console.log(req.path) // '/demo'
console.log(req.headers) // { host: 'localhost:3000', ... }
console.log(req.ip) // '127.0.0.1'
console.log(req.query) // { name: 'alice' } (from ?name=alice)
console.log(req.params) // { id: '42' } (from /demo/:id)
console.log(req.body) // parsed JSON or form data
res.send('ok')
})The Response Object (res)
res is the outgoing HTTP response. The most important methods:
// Send JSON (sets Content-Type: application/json automatically)
res.json({ user: 'alice' })
// Send a plain string or HTML
res.send('Hello World')
res.send('<h1>Hello</h1>')
// Set a status code
res.status(201).json({ created: true })
res.status(404).json({ error: 'Not found' })
res.status(500).json({ error: 'Internal server error' })
// Set a response header
res.set('X-Custom-Header', 'myvalue')
// Redirect
res.redirect('/new-url')
res.redirect(301, '/permanent-redirect')
// Send a file
res.sendFile(path.join(__dirname, 'public', 'index.html'))
// End the response with no body
res.status(204).end()HTTP Status Code Quick Reference
| Code | Meaning | When to use |
|---|---|---|
200 | OK | Successful GET, PUT, PATCH |
201 | Created | Successful POST that created a resource |
204 | No Content | Successful DELETE (no body) |
400 | Bad Request | Invalid input from client |
401 | Unauthorised | No or invalid authentication |
403 | Forbidden | Authenticated but no permission |
404 | Not Found | Resource does not exist |
409 | Conflict | Duplicate resource (e.g. email already taken) |
422 | Unprocessable Entity | Validation failed |
500 | Internal Server Error | Unexpected server error |
Route Parameters
Route parameters capture values from the URL path using :paramName:
// GET /users/42
app.get('/users/:id', (req, res) => {
const { id } = req.params
res.json({ userId: id, type: typeof id }) // id is always a string
})
// GET /posts/2026/may/nodejs-intro
app.get('/posts/:year/:month/:slug', (req, res) => {
const { year, month, slug } = req.params
res.json({ year, month, slug })
})Route params are always strings. Convert with
Number(id)orparseInt(id, 10)when you need a number.
Optional Parameters
// Both /products and /products/electronics match
app.get('/products/:category?', (req, res) => {
const category = req.params.category || 'all'
res.json({ category })
})Query Parameters
Query parameters (?key=value) are parsed automatically into req.query:
// GET /search?q=nodejs&limit=10&page=2
app.get('/search', (req, res) => {
const { q = '', limit = 20, page = 1 } = req.query
res.json({
query: q,
limit: Number(limit),
page: Number(page),
results: [], // would come from a database in a real app
})
})Handling POST Requests with a Request Body
// POST /users
// Body: { "name": "Alice", "email": "alice@example.com" }
app.post('/users', (req, res) => {
const { name, email } = req.body
// Basic validation
if (!name || !email) {
return res.status(400).json({ error: 'name and email are required' })
}
if (!email.includes('@')) {
return res.status(422).json({ error: 'Invalid email format' })
}
// In a real app: save to database
const newUser = { id: Date.now(), name, email, createdAt: new Date() }
res.status(201).json(newUser)
})Test it with curl:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'A Complete CRUD Example (In-Memory)
Here is a complete set of routes for a users resource using an in-memory array (we will connect to a real database in Module 14):
const express = require('express')
const app = express()
app.use(express.json())
// In-memory store (replaced by a database later)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
]
let nextId = 3
// GET /users — list all
app.get('/users', (req, res) => {
res.json(users)
})
// GET /users/:id — get one
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === Number(req.params.id))
if (!user) return res.status(404).json({ error: 'User not found' })
res.json(user)
})
// POST /users — create
app.post('/users', (req, res) => {
const { name, email } = req.body
if (!name || !email) {
return res.status(400).json({ error: 'name and email are required' })
}
const user = { id: nextId++, name, email }
users.push(user)
res.status(201).json(user)
})
// PUT /users/:id — full replace
app.put('/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === Number(req.params.id))
if (index === -1) return res.status(404).json({ error: 'User not found' })
const { name, email } = req.body
if (!name || !email) {
return res.status(400).json({ error: 'name and email are required' })
}
users[index] = { id: users[index].id, name, email }
res.json(users[index])
})
// PATCH /users/:id — partial update
app.patch('/users/:id', (req, res) => {
const user = users.find(u => u.id === Number(req.params.id))
if (!user) return res.status(404).json({ error: 'User not found' })
Object.assign(user, req.body)
res.json(user)
})
// DELETE /users/:id — remove
app.delete('/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === Number(req.params.id))
if (index === -1) return res.status(404).json({ error: 'User not found' })
users.splice(index, 1)
res.status(204).end()
})
app.listen(3000, () => console.log('Server on http://localhost:3000'))This is a fully functional CRUD API — five routes, proper status codes, and basic validation. You will connect it to MongoDB in Module 14.
Error Handling
Async Route Errors
If an async route throws, Express will not catch it automatically (in Express 4). You must either use try/catch or a wrapper:
// ❌ Unhandled promise rejection in Express 4
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id) // throws if DB is down
res.json(user)
})
// ✅ Option 1: try/catch
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.findUser(req.params.id)
res.json(user)
} catch (err) {
next(err) // forward to error handler
}
})
// ✅ Option 2: asyncHandler wrapper
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next)
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id)
res.json(user)
}))Note: Express 5 (currently in release candidate) catches async errors automatically — no wrapper needed.
The Error-Handling Middleware
Place a 4-parameter middleware at the very end of your app, after all routes:
// Must be LAST — after all routes and middleware
app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] ERROR:`, err.message)
const status = err.status || err.statusCode || 500
const message = err.expose ? err.message : 'Internal server error'
res.status(status).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
})
})404 Handler
Add a catch-all route before the error handler for unknown routes:
// 404 — must be after all routes, before error handler
app.use((req, res) => {
res.status(404).json({ error: `Route ${req.method} ${req.path} not found` })
})Serving Static Files
Express can serve static files (HTML, CSS, images, JS) from a directory:
const path = require('path')
// Serve everything in the 'public' folder
app.use(express.static(path.join(__dirname, 'public')))
// Now files like public/index.html are served at /index.html
// public/images/logo.png is served at /images/logo.pngProduction-Ready Server Template
Here is the full pattern you will use as the foundation for every Express project in this course:
// src/index.js
const express = require('express')
const path = require('path')
const app = express()
const PORT = process.env.PORT || 3000
// ── Body Parsers ──────────────────────────────────────
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// ── Static Files ──────────────────────────────────────
app.use(express.static(path.join(__dirname, '../public')))
// ── Routes ────────────────────────────────────────────
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date() })
})
// TODO: mount routers here
// app.use('/api/users', require('./routes/users'))
// ── 404 Handler ───────────────────────────────────────
app.use((req, res) => {
res.status(404).json({ error: `${req.method} ${req.path} not found` })
})
// ── Error Handler ─────────────────────────────────────
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
})
})
// ── Start ─────────────────────────────────────────────
app.listen(PORT, () => {
console.log(`🚀 Server running at http://localhost:${PORT}`)
})
module.exports = app // export for testingNode.js Full‑Stack Course — Module 9 of 32
You have built your first Express web server with full CRUD routes and error handling. Continue to Module 10 to design professional REST APIs following industry conventions.
Summary
Express.js transforms Node.js into a powerful, ergonomic web framework. Here is what to remember:
- Install with
npm install express— no configuration needed to get started app.use(express.json())parses incoming JSON bodies — always add this- Route methods:
app.get,app.post,app.put,app.patch,app.delete req.paramsfor URL segments,req.queryfor?key=value,req.bodyfor POST datares.json()for API responses,res.status(code)to set HTTP status- Always add a 404 handler and an error handler at the bottom of your app
- Use
try/catch+next(err)in async routes (or theasyncHandlerwrapper) - Export
appfrom your server file so you can import it in tests
Continue to Module 10: REST API Design with Express — Best Practices →
Content powered by SearchFit.ai — for automated content at scale, visit https://searchfit.ai
