← Back to list

From Zero to MERN: Episode 17: Query Parameters in Express — How Search, Filter, and Pagination…

Part 17 of the MERN Stack from Zero series. Read Part 16 — Route Parameters here

Minoltan Issack · 2026-06-28 00:34 · 0 claps · 6.1 min read
#expressjs #nodejs #fullstack-development #mern-stack #mern
Open on Medium ↗
Wiki topics: 🌐 · Web Development 📰 · Journalism & News

From Zero to MERN: Episode 17: Query Parameters in Express — How Search, Filter, and Pagination Actually Work

Part 17 of the MERN Stack from Zero series. Read Part 16 — Route Parameters here

You’ve built an endpoint that returns all users. You’ve built one that returns a single user by ID. But there’s a third scenario that neither of those handles — and it’s the most common one in real applications.

A user types “go” in a search box. Your app needs to return everyone whose username contains “go”. Not a specific ID. Not the full list. Something in between — filtered by a partial string the user provided.

That’s what query parameters do. They’re the ?filter=username&value=go part of the URL. Every search box, every sort dropdown, every pagination control you've ever used is built on this mechanism.

1. Route Params vs Query Params — Know When to Use Which

Before writing a single line, get this distinction locked in. You’ll make this decision on every endpoint you ever build.

┌──────────────────────────────────────────────────────────────────┐
│           ROUTE PARAMS vs QUERY PARAMS                          │
├───────────────────────────┬──────────────────────────────────────┤
│  Route Params             │  Query Params                        │
│  /api/users/:id           │  /api/users?filter=username&value=go │
├───────────────────────────┼──────────────────────────────────────┤
│  You know the exact ID    │  You're searching or filtering       │
│  Fetch one specific record│  Fetch a matching subset             │
│  ID is part of the path   │  Params come after the ?            │
│  req.params.id            │  req.query.filter, req.query.value   │
├───────────────────────────┼──────────────────────────────────────┤
│  Examples:                │  Examples:                           │
│  View user profile #42    │  Search users by name                │
│  Get product with ID 7    │  Filter products by category         │
│  Fetch order #1234        │  Sort by price, paginate results     │
└───────────────────────────┴──────────────────────────────────────┘

Think of how Netflix works: when you click on a specific show, that’s a route param (/shows/breaking-bad). When you type "game" in the search box and get partial matches, that's query params (/shows?search=game). Same API, different pattern for different intent.

2. How Express Reads Query Params — req.query

Take this URL:

localhost:3000/api/users?filter=username&value=go

Everything after the ? is the query string. Express automatically parses it and puts it into req.query:

app.get('/api/users', (req, res) => {
  console.log(req.query);
  // { filter: 'username', value: 'go' }
});

Just like req.params gives you route parameters, req.query gives you query parameters — as a plain JavaScript object. No manual parsing, no splitting on & and =. Express handles it.

Now destructure what you need:

app.get('/api/users', (req, res) => {
  const { filter, value } = req.query;
  console.log(filter);   // 'username'
  console.log(value);    // 'go'
});

filter tells you which field to search on. value is what to search for. These two together are everything you need to build a search endpoint.

3. The Filtering Logic — .filter() + .includes()

With the query extracted, use JavaScript’s native array methods to filter your data:

app.get('/api/users', (req, res) => {
  const { filter, value } = req.query;
  const result = users.filter(user =>
    user[filter].includes(value)
  );
  res.send(result);
});

Breaking this down:

  • users.filter(user => ...) — iterates every user, keeps those where the callback returns true
  • user[filter] — bracket notation to dynamically access the field named in filter. If filter = 'username', this reads user.username
  • .includes(value) — checks if the string contains value as a substring. 'comes'.includes('go') → no. 'logicwho'.includes('io') → yes

Test it:

/api/users?filter=username&value=go     → [{ id: 1, username: 'comes' }]
/api/users?filter=username&value=io     → [{ id: 1, username: 'logicio' }, { id: 4, username: 'codeio' }]
/api/users?filter=username&value=si     → [{ id: 2, username: 'siva' }]

Partial matching works. One endpoint, infinite search combinations.

4. The Case-Sensitivity Trap — Always Lowercase Before Comparing

Here’s a bug that catches everyone the first time:

Your data has username: 'Comes' (capital C). The URL query comes in as value=comes (lowercase — URLs are lowercase by convention).

'Comes'.includes('comes')   // false — case-sensitive mismatch

Zero results. No error. Just empty. Silent and confusing.

Fix it by normalizing to lowercase before comparing:

// ❌ Case-sensitive — breaks when user data has capitals
user[filter].includes(value)

// ✅ Normalize before comparing - case-insensitive search
user[filter].toLowerCase().includes(value.toLowerCase())
app.get('/api/users', (req, res) => {
  const { filter, value } = req.query;
const result = users.filter(user =>
    user[filter].toLowerCase().includes(value.toLowerCase())
  );
  res.send(result);
});

.toLowerCase() on both sides means it doesn't matter how the data is stored or how the user types their query. 'Comes' matches 'comes', 'COMES', 'CoMeS'. All the same.

5. Edge Cases — What If Filter or Value Is Missing?

What happens if someone hits /api/users?filter=username with no value?

const { filter, value } = req.query;
// filter = 'username'
// value = undefined
user[filter].toLowerCase().includes(value.toLowerCase())
// TypeError: Cannot read properties of undefined

Your server crashes. And what if someone hits /api/users with no query string at all? You should return all users — not crash.

Gate the filtering logic with a conditional:

app.get('/api/users', (req, res) => {
  const { filter, value } = req.query;
// Both filter AND value must be present to run the filter
  if (filter && value) {
    const result = users.filter(user =>
      user[filter].toLowerCase().includes(value.toLowerCase())
    );
    return res.send(result);
  }
  // No query params - return everything
  res.send(users);
});

Now the endpoint handles three cases cleanly:

/api/users                            → all users (no query)
/api/users?filter=username&value=go   → filtered subset
/api/users?filter=username            → all users (value missing, safe fallback)

Always code for the failure path, not just the happy path. When you write if (filter && value), you're saying: "I know this can be undefined, and I've handled it." That habit is what separates junior code from production code.

6. Expanding to Products — The Same Pattern, Different Data

The power of this pattern is how cleanly it replicates. Here’s the full products endpoint set:

// Dummy products data
const products = [
  { id: 1, productName: 'iPhone 17' },
  { id: 2, productName: 'S25 Ultra' },
  { id: 3, productName: 'S24 Plus' },
];
// Get all products (with optional filtering)
app.get('/api/products', (req, res) => {
  const { filter, value } = req.query;
  if (filter && value) {
    const result = products.filter(product =>
      product[filter].toLowerCase().includes(value.toLowerCase())
    );
    return res.send(result);
  }
  res.send(products);
});
// Get one product by ID
app.get('/api/products/:id', (req, res) => {
  const id = parseInt(req.params.id);
  if (isNaN(id)) {
    return res.status(400).send({ message: 'Bad Request: Invalid ID' });
  }
  const product = products.find(p => p.id === id);
  if (!product) {
    return res.status(404).send({ message: 'Product not found' });
  }
  res.send(product);
});

Test the query param filtering:

/api/products?filter=productName&value=17   → [{ id: 1, productName: 'iPhone 17' }]
/api/products?filter=productName&value=s    → [S25 Ultra, S24 Plus]
/api/products?filter=productName&value=2    → [S25 Ultra]
/api/products/2                             → { id: 2, productName: 'S25 Ultra' }

Same logic, different dataset. The only thing that changed was the variable names.

7. The Complete API — Both Collections, All Endpoints

// src/index.mjs
import express from 'express';
const app = express();
const port = 3000;
const users = [
  { id: 1, username: 'comes' },
  { id: 2, username: 'siva' },
  { id: 3, username: 'large' },
  { id: 4, username: 'codeio' },
];
const products = [
  { id: 1, productName: 'iPhone 17' },
  { id: 2, productName: 'S25 Ultra' },
  { id: 3, productName: 'S24 Plus' },
];

// ─── Root ─────────────────────────────────────────────────
app.get('/', (req, res) => res.send({ message: 'root' }));

// ─── Users ────────────────────────────────────────────────
app.get('/api/users', (req, res) => {
  const { filter, value } = req.query;
  if (filter && value) {
    return res.send(
      users.filter(u => u[filter].toLowerCase().includes(value.toLowerCase()))
    );
  }
  res.send(users);
});
app.get('/api/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  if (isNaN(id)) return res.status(400).send({ message: 'Bad Request: Invalid ID' });
  const user = users.find(u => u.id === id);
  if (!user) return res.status(404).send({ message: 'User not found' });
  res.send(user);
});

// ─── Products ─────────────────────────────────────────────
app.get('/api/products', (req, res) => {
  const { filter, value } = req.query;
  if (filter && value) {
    return res.send(
      products.filter(p => p[filter].toLowerCase().includes(value.toLowerCase()))
    );
  }
  res.send(products);
});
app.get('/api/products/:id', (req, res) => {
  const id = parseInt(req.params.id);
  if (isNaN(id)) return res.status(400).send({ message: 'Bad Request: Invalid ID' });
  const product = products.find(p => p.id === id);
  if (!product) return res.status(404).send({ message: 'Product not found' });
  res.send(product);
});

// ─── Start ────────────────────────────────────────────────
app.listen(port, () => console.log(`App running on port ${port}`));

Four endpoints. Two collections. Route params and query params. Full validation and error handling. This is the shape of a real REST API — just with dummy data instead of a database. That database connection is coming very soon.

Key Takeaways

1. Query params live in req.query — already parsed as a JavaScript object. No manual splitting or parsing. Express reads everything after ? and gives you a plain object.

2. Route params = exact lookup. Query params = search and filter. Know an ID? Use :id. Need partial matching, filtering, or sorting? Use ?filter=field&value=term.

3. Use bracket notation for dynamic field access. user[filter] lets you search any field without hardcoding. filter = 'username' becomes user.username at runtime.

4. Always .toLowerCase() before comparing. URLs deliver lowercase. Your data might be mixed case. Normalize both sides to prevent silent empty results.

5. Gate your filter logic with if (filter && value). If either is missing, fall back to returning the full collection. Never let undefined reach .toLowerCase().

6. The pattern replicates perfectly. Once you have working query param logic for one collection, copying it to another collection is a five-minute job. This is exactly how real REST APIs scale.

To stay informed on the latest technical insights and tutorials, connect with me on Medium and LinkedIn. For professional inquiries or technical discussions, please contact me via email. I welcome the opportunity to engage with fellow professionals and address any questions you may have.


메타데이터
post_id
d5f0f3c2fe3d
slug
from-zero-to-mern-episode-17-query-parameters-in-express-how-search-filter-and-pagination-d5f0f3c2fe3d
url
https://medium.com/@issackpaul95/from-zero-to-mern-episode-17-query-parameters-in-express-how-search-filter-and-pagination-d5f0f3c2fe3d
canonical_url
https://medium.com/@issackpaul95/from-zero-to-mern-episode-17-query-parameters-in-express-how-search-filter-and-pagination-d5f0f3c2fe3d
author_url
https://medium.com/@issackpaul95
status
ok
fetched_at
2026-07-07 04:24:22