# Creating Routes and Handling Requests with Express.js

Before Express.js existed, building an HTTP server in Node.js meant writing a lot of code just to get started.

## What Express.js Actually Is

Express is a minimal web framework for Node.js. That word minimal is important it doesn't come with an ORM, a templating engine, a validation library, or an authentication system built in. It comes with routing, middleware support, and a cleaner interface around Node's `http` module.

This is intentional. Express is a foundation, not a full solution. The developer community built the ecosystem around it hundreds of middleware packages that plug in cleanly so you compose exactly what you need, rather than inheriting what a framework decided you should have.

## Raw Node.js vs Express: The Problem It Solves

Let's see the gap side by side.

**Handling two routes in raw Node.js:**

```javascript
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: 'Home' }));
  } else 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.toString(); });
    req.on('end', () => {
      const data = JSON.parse(body);
      res.writeHead(201, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(data));
    });
  } else {
    res.writeHead(404);
    res.end('Not found');
  }
});

server.listen(3000);
```

![]( align="center")

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/d4d33640-9f17-4b5f-bf6b-d4072e6d4b23.png align="center")

This works. But notice what you're dealing with:

*   Manual `if/else` routing on every request
    
*   Checking method and URL manually every time
    
*   Manually reading the request body stream in chunks
    
*   Manually setting `Content-Type` headers on every response
    
*   No URL parameters (try matching `/users/42` — you'd need a regex or a URL parser)
    

**The same routes in Express:**

```javascript
const express = require('express');
const app = express();
app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Home' });
});

app.get('/users', (req, res) => {
  res.json({ users: [] });
});

app.post('/users', (req, res) => {
  res.status(201).json(req.body);
});

app.listen(3000);
```

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/cb120153-5e7a-4952-9820-ffb1c2082551.png align="center")

Express removes all the boilerplate. The routing is declarative. Body parsing is handled by middleware. `res.json()` sets `Content-Type` automatically. The intent of every route is immediately obvious.

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/1bfa43be-b73e-463f-a03e-4740dd131f51.png align="center")

## Installing Express

```shell
mkdir my-express-app
cd my-express-app
npm init -y
npm install express
```

Create `index.js` and you're ready.

## Your First Express Server

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

app.use(express.json());

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});
```

Three things happening here:

`require('express')` — imports the Express module. Express exports a function; calling it creates an application instance.

`app.use(express.json())` — registers middleware. This specific middleware reads the incoming request body and parses it as JSON, making it available as `req.body`. Without this line, `req.body` is `undefined` for POST requests. It only parses requests with `Content-Type: application/json` — it doesn't touch form submissions or plain text.

`app.listen(3000, callback)` — binds the server to port 3000. The callback fires once when the server is ready. This is exactly equivalent to creating an `http.Server` and calling `.listen()` on it.

Express does that for you internally.

## How Routing Works in Express

A route is the combination of an HTTP method and a URL path. When a request arrives, Express walks through your registered routes in the order you defined them and calls the first one that matches both the method and the path.

The basic signature is:

js

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

Where `METHOD` is `get`, `post`, `put`, `delete`, `patch`, or others. `PATH` is a string (or a regex, or an array). `HANDLER` is a function that receives `req` and `res`.

```javascript
app.get('/hello', (req, res) => {
  res.send('Hello, world');
});
```

If a `GET` request arrives for `/hello`, this handler runs. If the method is `POST`, it won't match. If the path is `/hello/world`, it won't match either (unless you use wildcards).

**What most tutorials don't explain:** the order of route definitions matters. Express matches routes top to bottom, first match wins. If you define a broad route before a specific one, the broad one catches the request and the specific one never runs.

```javascript
// Broad route defined first — always wins
app.get('/users/*', (req, res) => {
  res.send('wildcard'); // this matches /users/admin too
});

// This never runs — the route above caught it first
app.get('/users/admin', (req, res) => {
  res.send('admin panel');
});
```

Always define specific routes before general ones.

## Handling GET Requests

GET is the most common HTTP method fetch data, list resources, retrieve a single item.

```javascript
// Simple static route
app.get('/', (req, res) => {
  res.json({ status: 'ok', version: '1.0' });
});

// List resource
app.get('/products', (req, res) => {
  const products = [
    { id: 1, name: 'Keyboard', price: 79 },
    { id: 2, name: 'Mouse',    price: 39 },
  ];
  res.status(200).json(products);
});

// Single resource with URL parameter
app.get('/products/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const product = products.find(p => p.id === id);

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

  res.status(200).json(product);
});
```

`:id` in the path is a named URL parameter. Express captures whatever is in that position in the URL and puts it in [`req.params.id`](http://req.params.id). More on parameters in the next article for now, know they exist and look like `:name`.

## Handling POST Requests

POST is used to create new resources. Data comes in the request body, not the URL.

```javascript
app.post('/products', (req, res) => {
  const { name, price } = req.body;

  // Validate
  if (!name || price === undefined) {
    return res.status(400).json({ error: 'name and price are required' });
  }

  if (typeof price !== 'number' || price < 0) {
    return res.status(400).json({ error: 'price must be a non-negative number' });
  }

  // Create
  const newProduct = {
    id: Date.now(), // simple ID for demo
    name,
    price,
  };

  res.status(201).json(newProduct);
});
```

`req.body` works because `express.json()` middleware ran first and parsed the request body. If the client sends `Content-Type: application/json` and valid JSON in the body, `req.body` will be a JavaScript object.

The `return` before each error response is critical. Without it, Express attempts to send a second response after the first `res.json()` call, which throws a `"Cannot set headers after they are sent"` error. The `return` exits the handler function immediately.

What happens when the client sends invalid JSON? If the body is malformed, `express.json()` will respond with a `400` error automatically before your handler even runs. You don't need to handle JSON parse errors yourself — the middleware does it. But if you want to customize that error response (to match your API's error format), you can add an error-handling middleware:

```javascript
app.use((err, req, res, next) => {
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({ error: 'Invalid JSON in request body' });
  }
  next(err);
});
```

The four-parameter signature `(err, req, res, next)` is Express's convention for error-handling middleware. It only runs when an error is passed or thrown.

## Sending Responses

Express's `res` object extends Node's built-in response, adding helper methods that handle headers automatically.

`res.json(data)` — sends a JSON response. Sets `Content-Type: application/json` and `status 200` automatically. Calls `JSON.stringify` internally.

```javascript
res.json({ id: 1, name: 'Alice' });
```

`res.status(code)` — sets the HTTP status code. It returns `res` so you can chain it:

```javascript
res.status(201).json({ id: 1, name: 'Alice' }); // 201 Created
res.status(404).json({ error: 'Not found' });    // 404 Not Found
```

`res.send(body)` — sends a response. If you pass a string, `Content-Type` is `text/html`. If you pass an object, it behaves like `res.json()`. Prefer `res.json()` for APIs — it's explicit about the content type.

`res.status(204).send()` — sends an empty response. Required for DELETE endpoints where there's no body to return.

`res.set(header, value)` — sets a custom response header:

```javascript
res.set('X-Request-Id', 'abc123').json({ data: '...' });
```

**What most developers don't know about** `res.json()`**:** it runs `JSON.stringify` with the `json spaces` app setting. You can configure pretty-printing for development:

```javascript
app.set('json spaces', 2); // pretty-print JSON in development
```

This makes responses human-readable in development without changing your code. Set it conditionally:

```javascript
if (process.env.NODE_ENV !== 'production') {
  app.set('json spaces', 2);
}
```

## A Complete Working Server

Here's everything put together — a small but complete server covering multiple routes and both methods:

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

app.use(express.json());

// In-memory store
let books = [
  { id: 1, title: 'The Pragmatic Programmer', author: 'Hunt & Thomas' },
  { id: 2, title: 'Clean Code',               author: 'Robert Martin'  },
];
let nextId = 3;

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

// GET one book
app.get('/books/:id', (req, res) => {
  const book = books.find(b => b.id === parseInt(req.params.id));
  if (!book) return res.status(404).json({ error: 'Book not found' });
  res.status(200).json(book);
});

// POST create a book
app.post('/books', (req, res) => {
  const { title, author } = req.body;

  if (!title || !author) {
    return res.status(400).json({ error: 'title and author are required' });
  }

  const book = { id: nextId++, title, author };
  books.push(book);
  res.status(201).json(book);
});

// 404 fallback — must be last
app.use((req, res) => {
  res.status(404).json({ error: `Cannot ${req.method} ${req.url}` });
});

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

## Express Router: Splitting Routes Across Files

As an application grows, keeping every route in `index.js` becomes unmanageable. Express's `Router` is a mini-app — it has its own `.get()`, `.post()`, `.use()`, etc. — that you can mount on a path.

```javascript
// routes/books.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.json(books);
});

router.post('/', (req, res) => {
  // create book
});

router.get('/:id', (req, res) => {
  // get one book
});

module.exports = router;
```

```javascript
// index.js
const booksRouter = require('./routes/books');
app.use('/books', booksRouter);
```

![](https://cdn.hashnode.com/uploads/covers/6950d1ca85602739c40abd67/8003e517-026f-4a4e-a032-a35c5d79c69d.png align="center")

Routers are also just middleware. You can stack multiple routers, nest them, add middleware that only applies to a specific router, and reuse them across different mount points. The same `booksRouter` mounted at `/api/v1/books` and `/api/v2/books` would serve both paths from the same code.

Understand what Express does under the hood (it's mostly Node's `http` module with a layer on top) and you'll never be confused by its behavior. Understand how route matching works (top-down, first match wins) and you'll never write a route that silently never runs.

I’m currently deep-diving into the **JavaScript**, building projects and exploring the internals of the web. If you're on a similar journey or just love talking about JavaScript, let’s stay in touch!

*   **Connect on LinkedIn:** [**Satpalsinh's Profile**](https://www.linkedin.com/in/satpalsinhrana)
    
*   **Follow my Blog:** [**blogs.satpal.cloud**](http://blogs.satpal.cloud)
    

**Keep coding and keep building.**
