Two ways to pass data through URLs — and knowing when to use each one.
When I first started building APIs with Express, I kept confusing two things: when should the data go in the URL, and when should it go after the URL? Like, what's the difference between these two requests?
/users/42
/users?id=42
Both pass the number 42 to the server. Both work. But they mean fundamentally different things, and using the wrong one leads to messy, confusing APIs that other developers (and your future self) will hate.
The answer came down to a simple mental model I picked up in the ChaiCode Web Dev Cohort 2026: URL parameters identify what you want. Query strings modify how you want it. Once that clicked, I never confused them again.
Let me show you.
URL Structure Breakdown
Before diving into the differences, let's understand what a URL actually looks like:
https://api.example.com/users/42/orders?status=shipped&page=2
└─┬──┘ └──────┬──────┘└────┬────┘└─┬─┘ └──────────┬──────────┘
scheme host path params query string
Let's zoom in on the parts we care about:
/users/42/orders?status=shipped&page=2
│ │ │ │ │
│ │ │ └── query └── query
│ │ │ parameter parameter
│ │ │ key=value key=value
│ │ │
│ │ └── path segment
│ │
│ └── URL parameter (dynamic — changes per request)
│
└── path segment (static — always "users")
Two different mechanisms in the same URL. Let me explain each one.
What Are URL Parameters?
URL parameters (also called route parameters or path parameters) are dynamic segments inside the URL path. They're placeholders for specific values — typically identifiers.
In Express, you define them with a colon (:):
app.get("/users/:id", (req, res) => {
console.log(req.params.id); // whatever value is in that position
});
Examples
Route definition: /users/:id
/users/1 → req.params.id = "1"
/users/42 → req.params.id = "42"
/users/abc → req.params.id = "abc"
Route definition: /products/:category/:productId
/products/electronics/500
→ req.params.category = "electronics"
→ req.params.productId = "500"
/products/books/101
→ req.params.category = "books"
→ req.params.productId = "101"
Key Characteristics
- Part of the URL path (before the
?) - Defined with
:namein the route definition - Accessed via
req.params - Represent what resource you're requesting
- Usually required — the route won't match without them
Real-World URL Parameters
GET /users/42 → Get user with ID 42
GET /posts/15 → Get post with ID 15
GET /users/42/orders → Get all orders for user 42
GET /products/electronics/500 → Get product 500 in electronics
DELETE /comments/789 → Delete comment with ID 789
Notice the pattern: every URL parameter is an identifier — it points to a specific resource.
What Are Query Parameters?
Query parameters (also called query strings) are key-value pairs that come after the ? in a URL. They're used for filtering, sorting, searching, and modifying how the server processes the request.
In Express, you access them via req.query:
app.get("/users", (req, res) => {
console.log(req.query); // { role: "admin", page: "2" }
});
Examples
URL: /users?role=admin
req.query.role = "admin"
URL: /products?category=electronics&minPrice=100&maxPrice=500&sort=price
req.query.category = "electronics"
req.query.minPrice = "100"
req.query.maxPrice = "500"
req.query.sort = "price"
Key Characteristics
- Come after the
?in the URL - Format:
key=value, separated by& - Accessed via
req.query - Represent how to filter, sort, or modify the response
- Always optional — the route works without them
Real-World Query Parameters
GET /users?role=admin&active=true → Get active admin users
GET /products?sort=price&order=asc → Get products sorted by price
GET /search?q=nodejs&page=3&limit=20 → Search "nodejs", page 3, 20 results
GET /orders?status=shipped&from=2026-01 → Get shipped orders from January
GET /posts?tag=javascript → Get posts tagged "javascript"
Notice the pattern: every query parameter modifies the response — it doesn't change what resource you're requesting, it changes how that resource is filtered or presented.
Params vs Query — Comparison Diagram
┌───────────────────────────────────────────────────────────┐
│ │
│ URL PARAMETERS QUERY PARAMETERS │
│ (identifiers) (modifiers) │
│ │
│ /users/42 /users?role=admin │
│ ↑ ↑ │
│ "WHICH user?" "WHICH KIND of users?" │
│ → A specific one → A filtered list │
│ │
│ In the PATH After the ? │
│ req.params req.query │
│ Required Optional │
│ Identifies a resource Modifies the response │
│ │
│ Think: NOUN Think: ADJECTIVE │
│ "Give me user 42" "Give me admin users" │
│ │
└───────────────────────────────────────────────────────────┘
Accessing Params in Express
Single Parameter
// Route: /users/:id
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
// Find user (simulated)
const user = users.find((u) => u.id === parseInt(userId));
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json(user);
});
// GET /users/1 → { id: 1, name: "Pratham", role: "developer" }
// GET /users/999 → { error: "User not found" }
Multiple Parameters
// Route: /users/:userId/posts/:postId
app.get("/users/:userId/posts/:postId", (req, res) => {
const { userId, postId } = req.params;
res.json({
message: `Fetching post ${postId} by user ${userId}`,
});
});
// GET /users/5/posts/12
// → { message: "Fetching post 12 by user 5" }
Important: Params Are Always Strings
app.get("/users/:id", (req, res) => {
console.log(typeof req.params.id); // "string" — always!
// Convert to number when needed
const id = parseInt(req.params.id);
// Or check if it's a valid number
if (isNaN(id)) {
return res.status(400).json({ error: "Invalid user ID" });
}
// Now use the numeric id
});
Accessing Query Strings in Express
Basic Query
app.get("/search", (req, res) => {
const searchTerm = req.query.q;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
res.json({
searching: searchTerm,
page,
limit,
message: `Showing ${limit} results for "${searchTerm}" (page ${page})`,
});
});
// GET /search?q=express&page=2&limit=5
// → { searching: "express", page: 2, limit: 5, message: "..." }
// GET /search?q=nodejs
// → { searching: "nodejs", page: 1, limit: 10, message: "..." }
// (page and limit use defaults when not provided)
Filtering a Collection
const products = [
{ id: 1, name: "Laptop", category: "electronics", price: 75000 },
{ id: 2, name: "Shirt", category: "clothing", price: 1500 },
{ id: 3, name: "Phone", category: "electronics", price: 25000 },
{ id: 4, name: "Jeans", category: "clothing", price: 2500 },
{ id: 5, name: "Headphones", category: "electronics", price: 3000 },
];
app.get("/api/products", (req, res) => {
let result = [...products];
// Filter by category (optional)
if (req.query.category) {
result = result.filter((p) => p.category === req.query.category);
}
// Filter by max price (optional)
if (req.query.maxPrice) {
result = result.filter((p) => p.price <= parseInt(req.query.maxPrice));
}
// Sort by field (optional)
if (req.query.sort) {
result.sort((a, b) => {
if (req.query.order === "desc") return b[req.query.sort] - a[req.query.sort];
return a[req.query.sort] - b[req.query.sort];
});
}
res.json(result);
});
// GET /api/products
// → All 5 products
// GET /api/products?category=electronics
// → Laptop, Phone, Headphones
// GET /api/products?category=electronics&maxPrice=10000
// → Phone, Headphones
// GET /api/products?sort=price&order=desc
// → Sorted by price, highest first
Every query parameter is optional. The route works without any of them and returns all products. Add query parameters to narrow down the results.
Important: Query Values Are Always Strings
app.get("/example", (req, res) => {
// GET /example?count=5&active=true
console.log(typeof req.query.count); // "string"
console.log(typeof req.query.active); // "string"
// Convert as needed:
const count = parseInt(req.query.count); // 5 (number)
const active = req.query.active === "true"; // true (boolean)
});
When to Use Params vs Query — Decision Guide
| Scenario | Use | Example |
|---|---|---|
| Get a specific user | URL Param | GET /users/42 |
| Get all users filtered by role | Query String | GET /users?role=admin |
| Get a specific product | URL Param | GET /products/101 |
| Search products by keyword | Query String | GET /products?search=laptop |
| Get a specific order | URL Param | GET /orders/555 |
| Get orders filtered by status | Query String | GET /orders?status=shipped |
| Get a user's specific post | URL Params | GET /users/42/posts/7 |
| Get a user's posts sorted by date | Both | GET /users/42/posts?sort=date |
| Paginate a list | Query String | GET /posts?page=3&limit=20 |
| Delete a specific comment | URL Param | DELETE /comments/89 |
The Simple Rule
Ask yourself: "Am I identifying a SPECIFIC resource?"
→ YES → URL Parameter (/users/42)
→ NO → Query String (/users?role=admin)
Or even simpler:
→ REQUIRED identifier → URL Param
→ OPTIONAL modifier → Query String
Both Together — Common Pattern
Most real APIs use both in the same request:
// URL param identifies WHICH user
// Query string modifies WHICH posts to return
app.get("/users/:id/posts", (req, res) => {
const userId = req.params.id; // which user
const { tag, sort, page } = req.query; // how to filter posts
res.json({
user: userId,
filters: { tag, sort, page },
message: `Posts by user ${userId}, filtered by tag="${tag}", sorted by ${sort}, page ${page}`,
});
});
// GET /users/42/posts?tag=javascript&sort=newest&page=2
// → Posts by user 42, tagged "javascript", newest first, page 2
URL params tell you what. Query strings tell you how.
Common Mistakes
❌ Using Query Strings for Resource Identification
BAD: GET /users?id=42 ← id is required, should be a param
GOOD: GET /users/42 ← clean, RESTful
❌ Using URL Params for Optional Filters
BAD: GET /users/role/admin ← what if no filter is needed?
GOOD: GET /users?role=admin ← optional, clean
❌ Putting Everything in Query Strings
BAD: GET /api?resource=users&id=42&action=posts&postId=7
GOOD: GET /api/users/42/posts/7
❌ Forgetting to Parse Types
// Query values are always strings!
// GET /products?minPrice=100
// ❌ This comparison fails (string vs number)
if (product.price > req.query.minPrice) { ... }
// ✅ Parse the string to a number first
if (product.price > parseInt(req.query.minPrice)) { ... }
Let's Practice: Hands-On Assignment
Part 1: URL Parameters
const express = require("express");
const app = express();
const users = [
{ id: 1, name: "Pratham", role: "developer" },
{ id: 2, name: "Arjun", role: "designer" },
{ id: 3, name: "Priya", role: "manager" },
];
// Get user by ID
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);
});
app.listen(3000);
// Test: /users/1, /users/2, /users/99
Part 2: Query Strings
// Get users with optional filtering
app.get("/users", (req, res) => {
let result = [...users];
if (req.query.role) {
result = result.filter((u) => u.role === req.query.role);
}
if (req.query.search) {
result = result.filter((u) =>
u.name.toLowerCase().includes(req.query.search.toLowerCase()),
);
}
res.json({ count: result.length, users: result });
});
// Test: /users, /users?role=developer, /users?search=ar
Part 3: Combine Both
const posts = [
{ id: 1, userId: 1, title: "Learning Node.js", tag: "backend" },
{ id: 2, userId: 1, title: "Express Routing", tag: "backend" },
{ id: 3, userId: 2, title: "UI Design Tips", tag: "design" },
{ id: 4, userId: 1, title: "Async Patterns", tag: "backend" },
];
app.get("/users/:id/posts", (req, res) => {
const userId = parseInt(req.params.id);
let userPosts = posts.filter((p) => p.userId === userId);
if (req.query.tag) {
userPosts = userPosts.filter((p) => p.tag === req.query.tag);
}
res.json({
userId,
totalPosts: userPosts.length,
posts: userPosts,
});
});
// Test: /users/1/posts
// /users/1/posts?tag=backend
// /users/2/posts
Key Takeaways
-
URL parameters (
:id) are dynamic segments in the URL path. They identify which specific resource you want. Accessed viareq.params. -
Query parameters (
?key=value) come after the?in the URL. They filter, sort, or modify the response. Accessed viareq.query. - Params = identifiers, Query = modifiers. "Give me user 42" vs "Give me admin users."
-
Both values are always strings. Parse to numbers or booleans as needed with
parseInt()or=== "true". - Use params for required resource identification. Use query strings for optional filtering, sorting, pagination, and search.
Wrapping Up
The difference between URL parameters and query strings is one of those things that seems minor but shapes how clean and intuitive your API is. Using the right one in the right situation makes your routes readable, your API RESTful, and your code easy to maintain. Use params to identify. Use queries to modify. Combine them for powerful, flexible endpoints.
I'm learning all of this through the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg. Getting routing right early — especially the param vs query distinction — saves you from massive refactors later. It's a small detail with big impact.
Connect with me on LinkedIn or visit PrathamDEV.in. More articles on the way.
Happy coding! 🚀
Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode
Top comments (0)