Skip to main content

Command Palette

Search for a command to run...

The Era of Express: Routes, Params and the Art of Handling Requests

Updated
•10 min read•View as Markdown
The Era of Express: Routes, Params and the Art of Handling Requests
A
CS student and full-stack developer from Pakistan. Building with JavaScript, TypeScript, React, Next.js and Node.js. Currently in ChaiCode Web Dev Cohort 2026.

Introduction

Every backend does one job again and again: a request comes in, some logic runs, a response goes out. Node.js can do this on its own, but the code becomes long and repetitive very fast. Express exists to remove that pain.

In this blog, we will clearly understand how Express handles requests, like routes, handlers, responses, URL params and query strings.


What is Express and Why Do We Need It?

Express is a minimal web framework built on top of node.js. We can write node.js code without Express, but it becomes too repetitive and hard to manage when the project grows. That`s why TJ Holowaychuk took the http module of Node.js and built Express separately on top of it. With Express, we can create servers, write code efficiently, and handle routes easily, and keep our code organized as the project grows.

Let`s see a small server with raw Node.js (without express):

import http from 'http';
 
const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Home page');
  } else if (req.method === 'GET' && req.url === '/about') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('About page');
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not found');
  }
});
 
server.listen(3000);

Two routes, and it already looks heavy. Now imagine 40 routes, JSON bodies, URL parameters and query strings.

Now let`s see the same server in Express:

import express from 'express';
 
const app = express();
 
app.get('/', (req, res) => res.send('Home page'));
app.get('/about', (req, res) => res.send('About page'));
 
app.listen(3000, () => console.log('Server running on port 3000'));

As we can see, our code now looks much cleaner compared to the raw node.js server.Express gives a clear structure to our code, so it stays clean even when the project grows.

Comparing Raw Node.js and Express

Responsibility Raw Node.js http Express
Routing Manual if/else on req.url app.get(), app.post() etc.
Parsing URL params Manual req.params
Parsing query string Manual req.query
Sending JSON Set header + JSON.stringify yourself res.json()
Status codes res.writeHead() res.status()

Raw HTTP is manual, while Express makes it simple and fast.Express removes the boilerplate and makes routing easy.


Your First Express Server

npm init -y
npm install express
// server.js
import express from 'express';
 
const app = express();
const PORT = 3000;
 
app.get('/', (req, res) => {
  res.send('Hello from Express');
});
 
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Run it with node server.js and open http://localhost:3000.

Three things are happening here:

  1. express() creates the application object, app.

  2. app.get(...) registers a route.

  3. app.listen(...) starts the server and waits for requests.


The Request Lifecycle

Before writing more routes, understand the path a request takes:

  1. The client sends a request (method + URL).

  2. Express looks at its list of routes, from top to bottom.

  3. The first route that matches both the method and the path runs its handler.

  4. The handler sends a response, and the request ends.


Routing Basics

app.METHOD(PATH, HANDLER);
  • METHOD: the HTTP method (get, post, put, patch, delete).

  • PATH: the URL pattern to match.

  • HANDLER: the function that runs when both match. It receives req (request) and res (response).


Handling POST Requests

POST is used to create data. The data comes in the request body, and Express does not read a JSON body by default. We must enable it with the built-in express.json() middleware.

app.use(express.json());

app.post('/users', (req, res) => {
  const name = req.body.name;

  if (!name) {
    return res.status(400).json({ error: 'name is required' });
  }

  // save the user in the database

  res.status(201).json({ message: 'User created', name: name });
});

Notice three good habits here:

  • Validate the input, and return 400 if it is wrong.

  • Return 201 Created when something new is created.

  • Take only the fields you need (like name) from req.body. Don’t save the whole req.body directly, because the client can send extra fields.


PUT, PATCH and DELETE

//replace the whole resource
app.put('/users/:id', (req, res) => {
  const id = req.params.id;

  // find the user in the database
  // replace the whole user with the new data from req.body

  res.json({ message: `User ${id} replaced` });
});

// PATCH: update only some fields
app.patch('/users/:id', (req, res) => {
  const id = req.params.id;

  // find the user in the database
  // update only the fields sent in req.body

  res.json({ message: `User ${id} updated` });
});

// DELETE
app.delete('/users/:id', (req, res) => {
  const id = req.params.id;

  // find the user in the database
  // delete the user

  res.status(204).end(); // 204 = success, nothing to send back
});

Every handler must send a response. A handler that never responds leaves the client hanging until it times out.


Sending Responses

res gives us many ways to reply. These are the ones you will use daily:

Method What it does Example
res.send() Sends text, HTML, or an object/buffer res.send('Hello')
res.json() Sends JSON with the right Content-Type res.json({ ok: true })
res.status() Sets the status code (chainable) res.status(404).json({...})
res.sendStatus() Sets the status and sends its text res.sendStatus(200) sends OK
res.redirect() Redirects to another URL res.redirect(301, '/new')
res.type() Sets the Content-Type res.type('application/xml')
res.set() Sets a response header res.set('Request-Id', 'abc')
res.end() Ends the response without data res.status(204).end()

Rule: send only one response per request. Calling res.send() or res.json() twice throws the error "Cannot set headers after they are sent to the client". This usually happens when you forget a return:

// Wrong: both lines try to respond
if (!user) res.status(404).json({ error: 'Not found' });
res.json(user);
 
// Correct: return stops the function
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);

Common Status Codes

Code Meaning Use it when
200 OK Normal success
201 Created A new resource was created (POST)
204 No Content Success, nothing to return (DELETE)
400 Bad Request Invalid input
404 Not Found Resource or route does not exist
500 Server Error Something broke on the server

URL Params vs Query Strings

https://api.example.com/users/42/orders?status=pending&limit=10&page=2

It has three useful parts:

  • Path: /users/42/orders

  • URL param: 42 (a value inside the path)

  • Query string: ?status=pending&limit=10&page=2 (key-value pairs after the ?) Both carry data from the client to the server. But they carry different kinds of data.

URL Parameters (req.params)

A URL param is a dynamic part of the path. We define it with a colon (:) in the route.

app.get('/users/:id', (req, res) => {
  const id = Number(req.params.id);
 
  if (!Number.isInteger(id)) {
    return res.status(400).json({ error: 'id must be an integer' });
  }
 
  const user = users.find((u) => u.id === id);
  if (!user) return res.status(404).json({ error: 'User not found' });
 
  res.json(user);
});

A request to /users/1 gives req.params the value { id: '1' }.

You can have more than one param:

app.get('/users/:userId/orders/:orderId', (req, res) => {
  res.json(req.params); // { userId: '5', orderId: '9' }
});

Params identify a specific resource. /users/42 means "the user whose id is 42".


Query Strings (req.query)

A query string is everything after the ?. It is not part of the route definition. Express reads it for you and puts it in req.query.

app.get('/users', (req, res) => {
  const { role, city, limit = '10', page = '1' } = req.query;
 
  let result = users;
  if (role) result = result.filter((u) => u.role === role);
  if (city) result = result.filter((u) => u.city === city);
 
  const l = Number(limit);
  const p = Number(page);
 
  res.json({
    count: result.length,
    page: p,
    data: result.slice((p - 1) * l, p * l),
  });
});

A request to /users?role=student&limit=5 gives req.query the value { role: 'student', limit: '5' }.

Query strings filter, sort or paginate a collection. They are usually optional.


A Very Common Mistake: Everything is a String

Both req.params and req.query always give you strings. Even ?limit=5 gives '5', not 5. Convert it yourself:

const limit = Number(req.query.limit);

And always validate the result. Number('abc') is NaN, and that should become a 400, not a crash.

Key Differences

URL Params Query String
Example /users/42 /users?role=admin
Defined in route Yes (:id) No
Access with req.params req.query
Purpose Identify one resource Filter, sort, paginate
Required? Yes, route will not match without it Usually optional
Position Inside the path After the ?

When to Use Which

A simple rule that works almost always:

  • Which resource? Use a param. GET /users/42

  • How should I see the list? Use a query. GET /users?role=admin&page=2 You can combine both:

GET /users/42/orders?status=pending

The Code is saying: "Give me the orders of user 42, but only the pending ones."

The param picks the user, and the query filters the orders.

How to decide:

  • Use a param when the value tells you which resource you want. Example: /users/42

  • Use a query when the value tells you how you want the result. Example: /users?role=admin


Route Order Matters

Express checks your routes one by one, from top to bottom. As soon as one route matches, it runs that route and stops. It does not check the rest.

This can create a bug when a fixed route and a dynamic route look similar.

// Wrong order
app.get('/users/:id', (req, res) => res.json({ route: 'by id' }));
app.get('/users/search', (req, res) => res.json({ route: 'search' }));

Here, a request to /users/search reaches /users/:id first. Express treats the word search as the id, so the second route never runs.

This is how you can fix it: put the fixed route before the dynamic route.

// Correct order
app.get('/users/search', (req, res) => res.json({ route: 'search' }));
app.get('/users/:id', (req, res) => res.json({ route: 'by id' }));

Now /users/search matches the first route and stops. Any other value, like /users/42, moves down and matches /users/:id.

Rule: put fixed routes first, and dynamic routes (:id) after them.


Express Class Github Link: https://github.com/abdulrdeveloper/FullStackHub/tree/main/Learn%20Backend/Intro%20to%20Express/express-class


You can find more of my work at abdulrdeveloper.me

Read more posts at blog.abdulrdeveloper.me