Node.jsExpressBackendFull-Stack

Introduction to Express.js: Your First Web Server

TT
TopicTrick Team
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:

javascript
// ❌ 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:

javascript
// ✅ 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

bash
mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D nodemon

Update package.json scripts:

json
{
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  }
}

Create the file structure:

text
my-api/
├── src/
│   └── index.js
├── package.json
└── package-lock.json

Your First Express Server

Create src/index.js:

javascript
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:

bash
npm run dev

Open your browser or use curl:

bash
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:

javascript
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:

javascript
// 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

CodeMeaningWhen to use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST that created a resource
204No ContentSuccessful DELETE (no body)
400Bad RequestInvalid input from client
401UnauthorisedNo or invalid authentication
403ForbiddenAuthenticated but no permission
404Not FoundResource does not exist
409ConflictDuplicate resource (e.g. email already taken)
422Unprocessable EntityValidation failed
500Internal Server ErrorUnexpected server error

Route Parameters

Route parameters capture values from the URL path using :paramName:

javascript
// 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) or parseInt(id, 10) when you need a number.

Optional Parameters

javascript
// 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:

javascript
// 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

javascript
// 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:

bash
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):

javascript
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:

javascript
// ❌ 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:

javascript
// 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:

javascript
// 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:

javascript
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.png

Production-Ready Server Template

Here is the full pattern you will use as the foundation for every Express project in this course:

javascript
// 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 testing

Node.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.params for URL segments, req.query for ?key=value, req.body for POST data
    • res.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 the asyncHandler wrapper)
    • Export app from 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