# Creating Routes and Handling Requests with Express.js: A Practical Guide

# Creating Routes and Handling Requests with Express.js

**Audience:** This post assumes you know basic JavaScript and have Node.js installed. No prior Express experience required.

**TL;DR:** Express.js wraps Node's `http` module with a routing layer and middleware system. You define routes with `app.get()`, `app.post()`, etc., attach handler functions, and send responses with `res.json()` or `res.send()`. The result is readable, maintainable server code without manual URL parsing.

---

## Problem

Node.js ships with a built-in `http` module. It works, but it makes you do everything manually — parse URLs, check HTTP methods, route requests, set headers, and serialize response bodies. Here's what a minimal server looks like in raw Node:

```javascript
// raw-node-server.js
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/users') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ users: [] }));
  } else if (req.method === 'POST' && req.url === '/users') {
    let body = '';
    req.on('data', chunk => { body += chunk; });
    req.on('end', () => {
      const data = JSON.parse(body);
      res.writeHead(201, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ created: data }));
    });
  } else {
    res.writeHead(404);
    res.end('Not found');
  }
});

server.listen(3000, () => console.log('Server running on port 3000'));
```

This scales poorly. Adding five more routes turns into a deeply nested if-else tree. You're also manually streaming request bodies, setting headers on every response, and doing string matching on URLs.

Express solves all of this.

---

## What Express.js Is

Express is a minimal web framework for Node.js. It wraps the native `http` module and adds:

- **A routing layer** — match requests by method + path without if-else chains
- **Middleware pipeline** — run functions before your route handler (auth, body parsing, logging)
- **Response helpers** — `res.json()`, `res.status()`, `res.send()` instead of manual `writeHead` + `end`
- **Request parsing** — access `req.params`, `req.query`, `req.body` directly

It does not make architectural decisions for you. No ORM, no templating engine is forced on you. It's a routing + middleware layer, nothing more.

---

## Solution

### Step 1: Install Express

```bash
mkdir express-routing-demo && cd express-routing-demo
npm init -y
npm install express
```

This creates a project with Express as the only dependency. Node version 16+ is recommended.

---

### Step 2: Create Your First Express Server

```javascript
// server.js
const express = require('express');

const app = express();
const PORT = 3000;

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

app.listen(PORT, () => {
  console.log(`Server listening on http://localhost:${PORT}`);
});
```

`express()` returns an application object. `app.use(express.json())` registers a built-in middleware that parses incoming JSON bodies and attaches the result to `req.body`. Without this, `req.body` is `undefined` for POST requests with JSON payloads.

**Expected output when running `node server.js`:**
```
Server listening on http://localhost:3000
```

---

### Step 3: Handle GET Requests

Routes follow this pattern:

```
app.METHOD(PATH, HANDLER)
```

- `METHOD` — HTTP verb in lowercase (`get`, `post`, `put`, `delete`)
- `PATH` — URL string or pattern
- `HANDLER` — function that receives `(req, res)`

```javascript
// In-memory data store for this demo
const users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob',   email: 'bob@example.com' },
];

// GET /users — return all users
app.get('/users', (req, res) => {
  res.status(200).json({ users });
});

// GET /users/:id — return a single user by ID
app.get('/users/:id', (req, res) => {
  const userId = parseInt(req.params.id, 10);
  const user = users.find(u => u.id === userId);

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.status(200).json({ user });
});
```

**Key concepts here:**

- `req.params.id` — Express extracts the `:id` segment from the URL automatically. No manual URL parsing.
- `res.status(200).json({ user })` — chains the status code with the JSON response helper. Express sets the `Content-Type: application/json` header automatically.
- Returning early with `return res.status(404).json(...)` prevents the handler from continuing after sending the 404 response.

**Test it:**
```bash
curl http://localhost:3000/users
# {"users":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]}

curl http://localhost:3000/users/1
# {"user":{"id":1,"name":"Alice","email":"alice@example.com"}}

curl http://localhost:3000/users/99
# {"error":"User not found"}
```

---

### Step 4: Handle POST Requests

```javascript
// POST /users — create a new user
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 newUser = {
    id: users.length + 1,
    name,
    email,
  };

  users.push(newUser);

  res.status(201).json({ user: newUser });
});
```

`req.body` contains the parsed JSON payload because we registered `express.json()` middleware in Step 2. Without it, `req.body` would be `undefined` — a common mistake when starting with Express.

**Test it:**
```bash
curl -X POST http://localhost:3000/users \
  -H 'Content-Type: application/json' \
  -d '{"name": "Carol", "email": "carol@example.com"}'
# {"user":{"id":3,"name":"Carol","email":"carol@example.com"}}

curl -X POST http://localhost:3000/users \
  -H 'Content-Type: application/json' \
  -d '{"name": "Dave"}'
# {"error":"name and email are required"}
```

---

### Step 5: Complete Working Server

Here is the full file combining everything above:

```javascript
// server.js
const express = require('express');

const app = express();
const PORT = 3000;

app.use(express.json());

const users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob',   email: 'bob@example.com' },
];

// GET /users
app.get('/users', (req, res) => {
  res.status(200).json({ users });
});

// GET /users/:id
app.get('/users/:id', (req, res) => {
  const userId = parseInt(req.params.id, 10);
  const user = users.find(u => u.id === userId);

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.status(200).json({ user });
});

// POST /users
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 newUser = { id: users.length + 1, name, email };
  users.push(newUser);

  res.status(201).json({ user: newUser });
});

// 404 fallback — must be registered last
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

app.listen(PORT, () => {
  console.log(`Server listening on http://localhost:${PORT}`);
});
```

The 404 fallback is registered with `app.use()` at the end. Express processes routes in registration order — if no defined route matches, the request falls through to this handler.

---

### Request → Route Handler → Response Flow

```
Incoming HTTP Request
        │
        ▼
┌──────────────────────┐
│   express.json()     │  ← Middleware: parses body, attaches to req.body
└──────────────────────┘
        │
        ▼
┌──────────────────────┐
│   Route Matching     │  ← app.get('/users') / app.post('/users/:id')
│   (method + path)    │
└──────────────────────┘
        │
        ▼
┌──────────────────────┐
│   Route Handler      │  ← Your function: reads req, builds response
│   (req, res) => {}   │
└──────────────────────┘
        │
        ▼
┌──────────────────────┐
│   res.json() /       │  ← Sends HTTP response with headers + body
│   res.send()         │
└──────────────────────┘
        │
        ▼
    HTTP Response
```

---

## Results

Compare the same two-route implementation:

| Metric | Raw Node `http` | Express |
|---|---|---||
| Lines for 2 routes | ~28 lines | ~18 lines |
| Manual body streaming | Required | Handled by middleware |
| URL param extraction | Manual string split | `req.params.id` |
| JSON serialization | `JSON.stringify` + headers | `res.json()` |
| 404 handling | Manual else block | `app.use()` fallback |

As the number of routes grows, the gap widens significantly. A 10-route raw Node server typically requires 80-120 lines of routing logic alone. The same Express server stays under 60 lines.

---

## Trade-offs

**What Express adds on top of raw Node:**
- A small runtime overhead per request (~0.1–0.3ms on modern hardware for middleware processing). Negligible for most applications.
- An additional dependency. If Express has a vulnerability, your app inherits it.
- Middleware execution order matters — registering `express.json()` after your routes means request bodies won't be parsed in those routes. Bugs from ordering are subtle.

**What Express does NOT give you:**
- Input validation — use `zod` or `express-validator`
- Authentication — handle separately (Passport, JWT middleware)
- Database abstraction — no ORM included
- Request timeout handling — must be added explicitly

For very high-throughput internal services (100k+ req/s), frameworks like Fastify have a measurable performance advantage over Express due to a more efficient routing implementation and serialization layer. For most production APIs handling under 10k req/s, Express performance is not the bottleneck.

---

## Conclusion

Express reduces Node.js server code to its essential parts: define a route, read the request, send a response. The routing layer (`app.get`, `app.post`), automatic parameter extraction (`req.params`, `req.body`), and response helpers (`res.json`, `res.status`) eliminate the boilerplate that makes raw Node HTTP servers hard to maintain.

The next step is organizing routes using `express.Router()` — which lets you split routes into separate files and mount them on path prefixes, keeping your codebase modular as it grows.

---

## Further Reading

- [Express.js Official Routing Guide](https://expressjs.com/en/guide/routing.html) — Covers route parameters, query strings, and `express.Router`
- [Express Middleware Reference](https://expressjs.com/en/guide/using-middleware.html) — Explains the middleware pipeline in detail
- [Node.js HTTP Module Docs](https://nodejs.org/api/http.html) — Useful for understanding what Express abstracts
- [Fastify vs Express Benchmark](https://fastify.dev/benchmarks/) — Official Fastify benchmarks comparing throughput
- [express-validator](https://express-validator.github.io/docs/) — Input validation middleware for Express routes
