# Express.js: I Stopped Fighting Node's HTTP Module and Everything Got Easier

I built my first Node.js server using the built-in `http` module. It worked. It also made me want to close my laptop and go outside. Not because the code was hard, but because every tiny thing required writing the same boilerplate over and over. Want to check if the request is a GET to `/users`? You write an `if` statement. Want to parse the body of a POST request? You manually collect chunks of data from a stream. Want to send JSON back? You set headers by hand and call `res.end()` with a stringified object.

Here is what a basic server looks like with just Node:

```js
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/hello') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'hi there' }));
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('not found');
  }
});

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

Two routes and the code is already getting annoying. Imagine adding fifteen more. Imagine adding POST body parsing to each one. Imagine doing error handling across all of them. I tried. I got about four routes deep before the `if/else` chain became unreadable.

Express exists because nobody wants to write that.

* * *

## What Express actually is

Express is a library that sits on top of Node's `http` module. It does not replace it. Under the hood, Express still creates an `http.Server`. What it gives you is a cleaner way to define routes, read request data, and send responses without manually wiring everything together yourself.

Install it and set up a project:

```bash
mkdir my-express-app && cd my-express-app
npm init -y
npm install express
```

Now the same server from above, rewritten:

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

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

app.listen(3000, () => {
  console.log('running on port 3000');
});
```

No `writeHead`, or `JSON.stringify`. No checking `req.method` and `req.url` manually. One line defines the route, the method, and the handler. `res.json()` sets the content type and stringifies the object for you.

That difference gets wider the more routes you add.

![](https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/6cf063b3-9ac0-4d85-a8e3-b3b58ebc7061.png align="center")

The request comes in. Express checks its list of registered routes. If it finds a match for both the HTTP method and the URL path, it runs the handler function you gave it. If nothing matches, Express sends a 404 by default. You do not have to write that yourself.

* * *

## Handling GET requests

GET requests are how clients ask for data. When you type a URL in your browser, that is a GET request. When a frontend calls `fetch('/api/users')`, that is a GET request.

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

const users = [
  { id: 1, name: 'Priya', email: 'priya@example.com' },
  { id: 2, name: 'Ravi', email: 'ravi@example.com' },
  { id: 3, name: 'Meera', email: 'meera@example.com' },
];

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

app.listen(3000, () => console.log('running on 3000'));
```

Hit `http://localhost:3000/users` in your browser and you get:

```json
[
  { "id": 1, "name": "Priya", "email": "priya@example.com" },
  { "id": 2, "name": "Ravi", "email": "ravi@example.com" },
  { "id": 3, "name": "Meera", "email": "meera@example.com" }
]
```

Now say you want a single user by their ID. Express lets you put a colon in the URL path to create a route parameter:

```js
// get one user by id
app.get('/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const user = users.find(u => u.id === id);

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

  res.json(user);
});
```

`req.params.id` pulls the value from the URL. A request to `/users/2` gives you `req.params.id` as the string `"2"`. You parse it to a number, look it up, and either return the user or a 404. That `:id` syntax is called a route parameter, and you can have multiple in one path: `/users/:userId/orders/:orderId` would give you both `req.params.userId` and `req.params.orderId`.

One thing that tripped me up early: route parameters always come back as strings. Even if the URL says `/users/2`, `req.params.id` is `"2"`, not `2`. Forgetting to parse it leads to comparison bugs that are annoying to track down because `"2" === 2` is `false` in JavaScript.

* * *

## Handling POST requests

POST requests send data to the server. A signup form, a new order, a comment being submitted. The data lives in the request body, and Express does not parse it automatically. You need to tell it how.

```js
app.use(express.json());
```

That one line is middleware. It tells Express to parse incoming JSON bodies and attach the result to `req.body`. Without it, `req.body` is `undefined` and you will stare at your code wondering why nothing works. I spent 20 minutes on this the first time. Add this line near the top of your file, before your route definitions.

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

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

const users = [
  { id: 1, name: 'Priya', email: 'priya@example.com' },
  { id: 2, name: 'Ravi', email: 'ravi@example.com' },
];

let nextId = 3;

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: nextId++, name, email };
  users.push(newUser);

  res.status(201).json(newUser);
});

app.listen(3000, () => console.log('running on 3000'));
```

Test it with curl from your terminal:

```bash
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Aditya", "email": "aditya@example.com"}'
```

Response:

```json
{ "id": 3, "name": "Aditya", "email": "aditya@example.com" }
```

The `201` status code means "created." You could send `200` and it would work fine, but `201` tells the client specifically that a new resource was created. Using the right status codes is a habit worth building early. `200` means OK. `201` means created. `400` means the client sent bad data. `404` means not found. `500` means the server broke. Most real APIs use these consistently, and your code becomes easier to debug when the status code already tells you what category of problem you are looking at.

Here is what the full flow looks like when a POST request comes in:

![](https://cdn.hashnode.com/uploads/covers/678b775e773554ab7117f20a/0806d2c5-0752-4ea3-b6ac-830181d1e0cd.png align="center")

* * *

## Sending different types of responses

`res.json()` is the one you will use most, but Express has a few other response methods worth knowing.

```js
// send plain text
app.get('/health', (req, res) => {
  res.send('OK');
});

// send JSON with a status code
app.get('/status', (req, res) => {
  res.status(200).json({ status: 'running', uptime: process.uptime() });
});

// send just a status code with no body
app.delete('/users/:id', (req, res) => {
  // ... delete logic here
  res.sendStatus(204); // 204 means "no content" - deleted successfully, nothing to return
});

// redirect to another URL
app.get('/old-page', (req, res) => {
  res.redirect('/new-page');
});
```

`res.send()` is smart about what you give it. Pass a string, it sets the content type to `text/html`. Pass an object, it behaves like `res.json()`. Pass a Buffer, it sets the type to `application/octet-stream`. I would recommend being explicit and using `res.json()` for objects and `res.send()` for strings, rather than relying on the auto-detection. It makes your intent obvious when someone else reads the code later.

One gotcha: do not call `res.json()` or `res.send()` twice in the same handler. Express will throw an error that says "Cannot set headers after they are sent to the client." This usually happens when you forget a `return` statement before sending an error response:

```js
// broken - sends two responses
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));

  if (!user) {
    res.status(404).json({ error: 'not found' });
    // missing return! code keeps running
  }

  res.json(user); // crashes here because a response was already sent
});

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

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

  res.json(user);
});
```

That `return` before `res.status(404)` is easy to forget and hard to debug if you do not know what the error message means.

* * *

## Putting it all together

Here is a complete working server with GET and POST routes, input validation, proper status codes, and the JSON parsing middleware. You can copy this into a file called `server.js` and run it with `node server.js`:

```js
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

const users = [
  { id: 1, name: 'Priya', email: 'priya@example.com' },
  { id: 2, name: 'Ravi', email: 'ravi@example.com' },
];

let nextId = 3;

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

// get single user
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'user not found' });
  res.json(user);
});

// create a 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: nextId++, name, email };
  users.push(newUser);
  res.status(201).json(newUser);
});

// health check
app.get('/health', (req, res) => {
  res.send('OK');
});

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

Test it with these curl commands:

```bash
# get all users
curl http://localhost:3000/users

# get one user
curl http://localhost:3000/users/1

# create a user
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Aditya", "email": "aditya@example.com"}'

# try creating without required fields
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Aditya"}'
```

* * *

## A few habits worth picking up now

Use `nodemon` during development. It restarts your server automatically when you save a file. Without it you will be pressing Ctrl+C and typing `node server.js` hundreds of times a day.

```bash
npm install --save-dev nodemon
```

Add a script to your `package.json`:

```json
{
  "scripts": {
    "dev": "nodemon server.js"
  }
}
```

Then run `npm run dev` instead of `node server.js`.

Keep your route handlers short. If a handler is doing validation, database queries, and sending emails all in one function, it gets painful to maintain fast. As your app grows, pull logic into separate functions or files. You do not need to do this on day one, but keep it in mind as the file gets longer.

Always validate incoming data. Never trust `req.body` to contain what you expect. Check for missing fields. Check types when it matters. A missing `return` after sending an error response will crash your server, and bad input from a client should never be able to do that.

* * *

## What comes next

This covers the basics: setting up Express, handling GET and POST, sending responses, and avoiding the common early mistakes. The next piece you will run into is middleware, which is how Express handles things like authentication, logging, and error handling across routes without duplicating code everywhere. That will be its own post.

For now, build something small. A to-do list API. A bookmarks server. Something where you have two or three routes that create and read data. The concepts stick faster when you are solving a problem you actually care about, even a tiny one.

* * *

## References

*   [Express.js official docs](https://expressjs.com/)
    
*   [Express routing guide](https://expressjs.com/en/guide/routing.html)
    
*   [Express req and res API reference](https://expressjs.com/en/4x/api.html)
    
*   [MDN: HTTP status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
    
*   [nodemon on npm](https://www.npmjs.com/package/nodemon)
    
*   [Node.js http module docs](https://nodejs.org/api/http.html)
