Notifications

No notifications

/Phase 2

Express.js Basics

Express.js β€” The Standard Web Framework

Express is a minimal, unopinionated web framework for Node.js. It simplifies routing, middleware, and request handling.

Why Express?

Raw http moduleExpress
Manual routingapp.get('/path', handler)
Manual JSON parsingexpress.json() middleware
Manual error handlingBuilt-in error handler
No middleware chainRich middleware ecosystem

Basic Setup

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

// Parse JSON bodies app.use(express.json());

// Define routes app.get('/', (req, res) => { res.json({ message: 'Hello World!' }); });

app.listen(3000, () => console.log('Server on port 3000'));

Request Object (req)

PropertyDescriptionExample
req.paramsRoute parameters/users/:id β†’ req.params.id
req.queryQuery string?page=1 β†’ req.query.page
req.bodyRequest bodyPOST JSON data
req.headersRequest headersreq.headers['authorization']
req.methodHTTP method'GET', 'POST'

Response Object (res)

MethodDescription
res.json(data)Send JSON response
res.status(code)Set status code
res.send(data)Send any response
res.redirect(url)Redirect client

On this page

Detailed Theory

What Express Is and Why Everyone Uses It

Node's built-in http module works, but writing a real API with it is painful β€” you'd parse URLs by hand, dispatch on method strings, and reinvent middleware. Express is a tiny framework on top of http that gives you:

  • Clean routing (app.get('/users', …))
  • Middleware (functions that run on every request)
  • A pleasant request / response API
  • A massive ecosystem of plug-in middlewares (auth, logging, CORS, validation…)
It's been the default Node web framework for over a decade. If you can read Express, you can read 80% of all Node backends.

Hello, Express

npm install express

// server.js
import express from 'express';

const app = express();

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

app.listen(3000, () => console.log('http://localhost:3000'));

Three concepts:

  • express() builds the app object.
  • app.get(path, handler) registers a route.
  • app.listen(port) opens the socket.

The req and res Objects

Every handler receives req (incoming) and res (outgoing).

app.get('/users/:id', (req, res) => {
  req.params.id    // "42"          (URL params)
  req.query.page   // "3"           (?page=3)
  req.body         // { ... }       (after express.json())
  req.headers      // { 'authorization': '...' }
  req.method       // "GET"
  req.path         // "/users/42"
  req.ip           // client IP
});

res.send('hi');                  // text/html
res.json({ ok: true });          // JSON
res.status(404).json({ ... });   // chain status + body
res.redirect('/login');
res.set('X-Total', '42');        // set header
res.cookie('session', 'abc');
res.sendFile('/path/to/file');

Routes for Each HTTP Method

app.get   ('/users',     listUsers);
app.post  ('/users',     createUser);
app.put   ('/users/:id', replaceUser);
app.patch ('/users/:id', updateUser);
app.delete('/users/:id', deleteUser);

Match an HTTP method + URL path β†’ run a function. That's the whole programming model.

Reading JSON From the Body

Express doesn't parse bodies by default β€” you opt in with built-in middleware:

app.use(express.json());                       // parse application/json
app.use(express.urlencoded({ extended: true }));// parse form posts

app.post('/users', (req, res) => { const { name, email } = req.body; res.status(201).json({ id: 1, name, email }); });

Beginner Mistakes to Skip

1. req.body is undefined. You forgot app.use(express.json()). (Or the client forgot Content-Type: application/json.) 2. Sending a response twice. res.json(…) then res.send(…) throws Cannot set headers after they are sent. Always return after sending. 3. Forgetting return after res.status(404).json(…) β€” code below keeps running and sends a second response. 4. Using process.env.PORT without a fallback. app.listen(process.env.PORT || 3000) is the safe pattern. 5. Hard-coding origins for CORS. Use the cors package and a config-driven allowlist. 6. Putting all routes in server.js. Split by feature into routes/users.js, routes/posts.js once you have more than ~5.

Intermediate: The Request Lifecycle

Every request walks top to bottom through whatever you registered with app.use and app.METHOD:

incoming request
      ↓
express.json()        ← parse body
cors()                ← add CORS headers
morgan('dev')         ← log
authMiddleware()      ← verify JWT, set req.user
router / handler      ← your route function runs
errorHandler(err,...) ← only if next(err) was called

Order matters: an auth middleware after the route can't protect it.

Intermediate: Project Layout (Once It Grows)

src/
β”œβ”€β”€ server.js                  ← boot the app
β”œβ”€β”€ app.js                     ← build the express app (testable!)
β”œβ”€β”€ config/
β”œβ”€β”€ middleware/
β”‚   β”œβ”€β”€ auth.js
β”‚   └── errorHandler.js
β”œβ”€β”€ routes/
β”‚   β”œβ”€β”€ users.routes.js
β”‚   └── posts.routes.js
β”œβ”€β”€ controllers/             ← thin: parse req, call service, send res
β”œβ”€β”€ services/                ← business logic
└── models/                  ← DB layer

Rule of thumb: routes are dumb, services are smart. Controllers translate HTTP to function calls.

Intermediate: Routers β€” Mini-Apps

// routes/users.routes.js
import { Router } from 'express';
const router = Router();

router.get('/', listUsers); router.post('/', createUser); router.get('/:id', getUser);

export default router;

// app.js
import usersRouter from './routes/users.routes.js';
app.use('/api/users', usersRouter);

Routers are how you split a big API into small, mountable pieces.

Intermediate: Async Handlers

Express 4 doesn't catch errors thrown from async functions. Two fixes:

// 1. try/catch + next
app.get('/users/:id', async (req, res, next) => {
  try {
    const u = await db.users.find(req.params.id);
    if (!u) return res.status(404).json({ error: 'not found' });
    res.json(u);
  } catch (err) { next(err); }
});

// 2. wrap once with a helper const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); app.get('/users/:id', asyncH(async (req, res) => { /* … */ }));

Express 5 (now stable) finally handles thrown async errors automatically.

Intermediate: Central Error Handler

app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({
    error: err.message,
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
  });
});

Four-arg signature β†’ Express recognises it as an error handler. Always last.

Advanced: Production Hardening

  • helmet() β€” sane default security headers.
  • compression() β€” gzip/brotli responses.
  • express-rate-limit β€” throttle abusive IPs.
  • cookie-parser + csurf β€” CSRF defence (when using cookies).
  • pino-http or morgan β€” structured request logs.
  • Run behind a reverse proxy (NGINX, Caddy, Cloudflare) and app.set('trust proxy', 1) so req.ip is correct.

Advanced: Graceful Shutdown

Deploy systems send SIGTERM before killing your container. Stop accepting new requests, finish in-flight ones, then exit:

const server = app.listen(3000);

for (const sig of ['SIGINT', 'SIGTERM']) { process.on(sig, () => { server.close(() => process.exit(0)); setTimeout(() => process.exit(1), 10_000).unref(); // hard kill after 10s }); }

Advanced: Express vs the Newer Wave

  • Fastify β€” same idea, ~2Γ— faster, schema-driven validation, plugin system.
  • Hono / Elysia β€” modern, edge-runtime-friendly, TypeScript-first.
  • NestJS β€” opinionated framework on top of Express/Fastify; classes, DI, decorators (Angular-style).
Express is still the "lingua franca". Learn it first; reach for the others when you have a specific need.

Practice Path

1. Build a 3-route Express app: GET /, GET /users, POST /users (in-memory array). 2. Move the user routes into routes/users.routes.js using Router. 3. Add express.json(), cors(), and a 404 handler at the end. 4. Add an async controller that throws on bad input and a central error handler that maps it to 400.