DEV Community

Cover image for Express.js Routing — A Practical Guide with a Real Project
Chinwuba
Chinwuba

Posted on

Express.js Routing — A Practical Guide with a Real Project

If you've just started with Express, routing is the first concept that makes everything feel real. This isn't a theory post — we're going to break it down and build something with it.

What is Routing

Routing is how your Express server decides what to do when a request comes in. Every request has two things — an HTTP method and a URL path. Routing matches those two things to a specific piece of code.

HTTP Methods

Before writing any routes, understand what each method represents:
GET → Fetch data
POST → Create something new
PUT → Update something
DELETE → Remove something

These aren't Express rules. They're the internet's conventions. Express just gives you a clean way to hook into them.

Writing Your First Routes
javascript
const express = require('express');
const app = express();

app.use(express.json());

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

app.post('/users', (req, res) => {
  const newUser = req.body;
  res.send(`Created: ${newUser.name}`);
});

app.put('/users', (req, res) => {
  res.send('Updated user');
});

app.delete('/users', (req, res) => {
  res.send('Deleted user');
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Same URL, four completely different behaviors based on the method. That's the core idea.

Route Params

What if you need to target a specific user, not all of them? Route params handle that.

javascript
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`User: ${userId}`);
});
Enter fullscreen mode Exit fullscreen mode

The colon makes :id a dynamic placeholder. Hit /users/42 and Express captures 42 and hands it to you via req.params.id.

Query Strings

Query strings are the ?key=value parts of a URL. They're optional extras used for filtering or sorting.

javascript
app.get('/users', (req, res) => {
  const { role, active } = req.query;
  res.send(`Role: ${role}, Active: ${active}`);
});
Enter fullscreen mode Exit fullscreen mode

The difference between params and query strings is simple — params are required for the route to match, query strings are optional.

Route Grouping with Router()

As your app grows, dumping every route into app.js becomes unmanageable. Router() lets you split routes by feature into separate files.

routes/users.js

javascript
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => res.send('All users'));
router.get('/:id', (req, res) => res.send(`User ${req.params.id}`));
router.post('/', (req, res) => res.send('Created'));
router.delete('/:id', (req, res) => res.send(`Deleted ${req.params.id}`));

module.exports = router;

index.js
javascript
const express = require('express');
const UserRouter = require('./routes/users');

const app = express();
app.use(express.json());
app.use('/users', UserRouter);

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Everything in UserRouter is now prefixed with /users automatically. You create a separate file for each feature — users, posts, products — and app.js stays clean.
Building a Real Example
Let's wire this up with a fake database — just a JavaScript array — so the routes actually do something.

javascript
const express = require('express');
const router = express.Router();

let users = [
  { id: 1, name: "Jeffrey" },
  { id: 2, name: "Chidi" },
  { id: 3, name: "Amara" }
];

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

// GET single user
router.get('/:id', (req, res) => {
  const userId = Number(req.params.id);
  const user = users.find(u => u.id === userId);
  if (user) {
    res.json(user);
  } else {
    res.status(404).send('User not found');
  }
});

// POST create user
router.post('/', (req, res) => {
  const newUser = { id: users.length + 1, name: req.body.name };
  users.push(newUser);
  res.json(newUser);
});

// PUT update user
router.put('/:id', (req, res) => {
  const userId = Number(req.params.id);
  const user = users.find(u => u.id === userId);
  if (user) {
    user.name = req.body.name;
    res.json(user);
  } else {
    res.status(404).send('User not found');
  }
});

// DELETE user
router.delete('/:id', (req, res) => {
  const userId = Number(req.params.id);
  users = users.filter(u => u.id !== userId);
  res.send(`Deleted user ${userId}`);
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

A few things worth noting from building this:

The client should never send an ID on POST — your server generates it. Use users.length + 1 or a proper ID generator like uuid in production.
When you filter an array in your DELETE route, reassign it back — users = users.filter(...) — otherwise nothing actually gets deleted.
Always convert req.params.id to a Number before comparing with ===. Route params come in as strings by default, and "1" === 1 is false in JavaScript.
The Mental Model
Method + URL = one specific action

GET /users → list all
POST /users → create one
GET /users/:id → get one
PUT /users/:id → update one
DELETE /users/:id → delete one
This pattern is REST. Almost every API you'll ever build or consume follows it. Once it's in your head, reading and building APIs becomes second nature.

Top comments (0)