URL Parameters vs Query Strings in Express.js

Every URL tells a story. Part of that story is built into the path itself. Another part is hidden in the query string. Learn the difference and you'll write cleaner, more predictable APIs.
This is about URL parameters, query strings, how to use them in Express, and when to pick one over the other.
What Are URL Parameters?
URL parameters are placeholders in the path itself. They're part of the route structure.
/users/123
^^^
This is a URL parameter
The 123 is the parameter. It's the actual data embedded in the path.
How They Look
/users/123
/posts/42
/products/coffee-maker
/users/john/posts/15
The value changes, but the structure stays the same. You're identifying a specific resource.
In Express
app.get("/users/:userId", (req, res) => {
const userId = req.params.userId;
res.json({ userId });
});
When someone hits /users/123, req.params.userId equals "123".
What Are Query Strings?
Query strings come after the ? in the URL. They're optional filters or modifiers.
/users?role=admin&active=true
^^^^^^^^^^^^^^^^^^^^^^^^
This is the query string
Query strings don't change the resource you're asking for. They modify how you get it.
How They Look
/users?role=admin
/posts?sort=date&order=desc
/products?category=electronics&price=100-500
/search?q=javascript&limit=10
Multiple query parameters are joined with &. Order doesn't matter.
In Express
app.get("/users", (req, res) => {
const role = req.query.role;
const active = req.query.active;
res.json({ role, active });
});
When someone hits /users?role=admin&active=true, req.query contains:
{
role: "admin",
active: "true"
}
URL Structure Breakdown
Here's what a complete URL looks like:
https://example.com/users/123/posts/42?sort=date&limit=10
| | | | | | |
| | | | | | Query parameter
| | | | | Query parameter
| | | | Query string (starts with ?)
| | | URL parameter
| | URL parameter
| Path
Domain
Breaking it down:
- Domain:
example.com - Path:
/users/123/posts/42 - URL Parameters:
123and42 - Query String:
sort=date&limit=10 - Query Parameters:
sortandlimit
URL Parameters vs Query Strings
URL Parameters
For identifying a specific resource or navigating a hierarchy.
/users/123 <- Get user with ID 123
/posts/42 <- Get post with ID 42
/users/123/posts/42 <- Get post 42 belonging to user 123
/products/electronics/phones <- Get phones in electronics category
Use URL parameters when:
- You're identifying a specific resource
- The parameter is essential to the request
- You're navigating a hierarchy
- The value is part of the resource's unique identity
Query Strings
For filtering, sorting, pagination, or optional modifications.
/users?role=admin <- Get users, filter by role
/posts?sort=date&order=desc <- Get posts, sort by date
/products?page=2&limit=20 <- Get products, pagination
/search?q=javascript <- Search for "javascript"
Use query strings when:
- You're filtering or searching
- The parameter is optional
- You're modifying how data is presented
- Multiple parameters can apply simultaneously
Quick Comparison
| Aspect | URL Parameters | Query Strings |
|---|---|---|
| Location | In the path | After ? |
| Purpose | Identify resource | Filter or modify |
| Essential | Usually required | Optional |
| Example | /users/123 |
/users?role=admin |
| Multiple | /users/123/posts/42 |
?a=1&b=2&c=3 |
| Order | Fixed | Doesn't matter |
| Best for | IDs and hierarchy | Filters and options |
Accessing URL Parameters in Express
Single Parameter
app.get("/users/:userId", (req, res) => {
const userId = req.params.userId;
res.json({ userId });
});
Request: GET /users/123
Result: userId = "123"
Multiple Parameters
app.get("/users/:userId/posts/:postId", (req, res) => {
const userId = req.params.userId;
const postId = req.params.postId;
res.json({ userId, postId });
});
Request: GET /users/123/posts/42
Result: userId = "123", postId = "42"
Named Parameters
app.get("/products/:category/:productId", (req, res) => {
const { category, productId } = req.params;
res.json({ category, productId });
});
Request: GET /products/electronics/phone-456
Result: category = "electronics", productId = "phone-456"
Accessing All Parameters
app.get("/users/:userId/posts/:postId/comments/:commentId", (req, res) => {
console.log(req.params);
// { userId: "123", postId: "42", commentId: "789" }
res.json(req.params);
});
Accessing Query Strings in Express
Single Query Parameter
app.get("/users", (req, res) => {
const role = req.query.role;
res.json({ role });
});
Request: GET /users?role=admin
Result: role = "admin"
Multiple Query Parameters
app.get("/posts", (req, res) => {
const sort = req.query.sort;
const order = req.query.order;
const limit = req.query.limit;
res.json({ sort, order, limit });
});
Request: GET /posts?sort=date&order=desc&limit=10
Result: sort = "date", order = "desc", limit = "10"
Using Destructuring
app.get("/search", (req, res) => {
const { q, category, page } = req.query;
res.json({ q, category, page });
});
Request: GET /search?q=javascript&category=tutorials&page=2
Result: q = "javascript", category = "tutorials", page = "2"
Accessing All Query Parameters
app.get("/filter", (req, res) => {
console.log(req.query);
// All query parameters as an object
res.json(req.query);
});
Request: GET /filter?a=1&b=2&c=3
Result:
{
a: "1",
b: "2",
c: "3"
}
Handling Missing Parameters
Query parameters might not exist. Always check:
app.get("/products", (req, res) => {
const page = req.query.page || 1;
const limit = req.query.limit || 10;
res.json({ page, limit });
});
Request: GET /products
Result: page = 1, limit = 10
Real-World Examples
Example 1: User Profile
// Get a specific user
app.get("/users/:userId", (req, res) => {
const userId = req.params.userId;
const user = findUserById(userId);
res.json(user);
});
Request: GET /users/123
Purpose: Get user 123's profile
Example 2: Search with Filters
// Search users with optional filters
app.get("/users/search", (req, res) => {
const { q, role, active } = req.query;
const results = searchUsers(q, {
role,
active
});
res.json(results);
});
Request: GET /users/search?q=john&role=admin&active=true
Purpose: Search for "john", filter by role and active status
Example 3: Nested Resources
// Get posts by a specific user
app.get("/users/:userId/posts", (req, res) => {
const userId = req.params.userId;
const posts = getPostsByUser(userId);
res.json(posts);
});
// Get a specific post by a specific user
app.get("/users/:userId/posts/:postId", (req, res) => {
const { userId, postId } = req.params;
const post = getPost(userId, postId);
res.json(post);
});
Requests:
GET /users/123/posts→ Get all posts by user 123GET /users/123/posts/42→ Get post 42 by user 123
Example 4: Pagination and Sorting
// Get all products with optional pagination and sorting
app.get("/products", (req, res) => {
const page = req.query.page || 1;
const limit = req.query.limit || 20;
const sort = req.query.sort || "name";
const order = req.query.order || "asc";
const products = getProducts({
page,
limit,
sort,
order
});
res.json(products);
});
Requests:
GET /products→ Default paginationGET /products?page=2&limit=10→ Page 2, 10 itemsGET /products?sort=price&order=desc→ Sort by price, descending
Example 5: Combined Parameters and Query
// Get posts by a user with optional sorting and pagination
app.get("/users/:userId/posts", (req, res) => {
const userId = req.params.userId;
const page = req.query.page || 1;
const sort = req.query.sort || "date";
const posts = getPostsByUser(userId, {
page,
sort
});
res.json(posts);
});
Requests:
GET /users/123/posts→ Get user 123's posts, default sortingGET /users/123/posts?sort=likes&page=2→ User 123's posts, sorted by likes, page 2
When to Use URL Parameters vs Query Strings
Use URL Parameters When
You're identifying a specific resource:
// Get specific user
app.get("/users/:userId", (req, res) => {
// userId is essential
});
// Get specific post
app.get("/posts/:postId", (req, res) => {
// postId is essential
});
// Hierarchical navigation
app.get("/users/:userId/posts/:postId", (req, res) => {
// Both are essential to identify the resource
});
Use Query Strings When
You're filtering, sorting, or modifying results:
// Filter by role
app.get("/users", (req, res) => {
// role is optional
});
// Sort results
app.get("/posts", (req, res) => {
// sort is optional
});
// Pagination
app.get("/products", (req, res) => {
// page and limit are optional
});
// Search
app.get("/search", (req, res) => {
// q is optional but the endpoint makes sense without it
});
Decision Tree
Is this parameter essential to identify the resource?
├─ YES → Use URL parameter
│ /users/123
│ /posts/42
│ /products/electronics/phones
│
└─ NO → Use query string
/users?role=admin
/posts?sort=date
/products?page=2
Complete Example: Blog API
Here's a real blog API using both URL parameters and query strings:
const express = require("express");
const app = express();
// Get all posts with optional filters
app.get("/posts", (req, res) => {
const { author, status, page, limit, sort } = req.query;
const posts = filterPosts({
author,
status,
page: page || 1,
limit: limit || 10,
sort: sort || "date"
});
res.json(posts);
});
// Get a specific post
app.get("/posts/:postId", (req, res) => {
const { postId } = req.params;
const post = getPost(postId);
if (!post) {
return res.status(404).json({ error: "Post not found" });
}
res.json(post);
});
// Get comments on a post
app.get("/posts/:postId/comments", (req, res) => {
const { postId } = req.params;
const { page, limit, sort } = req.query;
const comments = getComments(postId, {
page: page || 1,
limit: limit || 20,
sort: sort || "date"
});
res.json(comments);
});
// Get a specific comment on a specific post
app.get("/posts/:postId/comments/:commentId", (req, res) => {
const { postId, commentId } = req.params;
const comment = getComment(postId, commentId);
if (!comment) {
return res.status(404).json({ error: "Comment not found" });
}
res.json(comment);
});
// Get posts by a specific author
app.get("/authors/:authorId/posts", (req, res) => {
const { authorId } = req.params;
const { page, status } = req.query;
const posts = getPostsByAuthor(authorId, {
page: page || 1,
status
});
res.json(posts);
});
// Create a new post
app.post("/posts", (req, res) => {
const { title, content, author } = req.body;
const newPost = createPost({ title, content, author });
res.status(201).json(newPost);
});
app.listen(3000);
Example Requests
GET /posts
→ Get all posts, default pagination
GET /posts?author=john&status=published
→ Get published posts by john
GET /posts?page=2&limit=5&sort=views
→ Get posts on page 2, 5 per page, sorted by views
GET /posts/42
→ Get post 42
GET /posts/42/comments
→ Get comments on post 42
GET /posts/42/comments?page=1&sort=likes
→ Get comments on post 42, sorted by likes
GET /posts/42/comments/789
→ Get comment 789 on post 42
GET /authors/john/posts
→ Get all posts by author john
GET /authors/john/posts?status=draft&page=1
→ Get draft posts by john, page 1
Practice Assignment
1. Create a user endpoint with URL parameters:
const express = require("express");
const app = express();
// Create this endpoint
app.get("/users/:userId", (req, res) => {
// Get the userId from params
// Return the user object
});
app.listen(3000);
2. Add a search endpoint with query strings:
// Create this endpoint
app.get("/users/search", (req, res) => {
// Get query parameters: q, role, active
// Return filtered results
});
3. Create a nested resource endpoint:
// Get all posts by a user
app.get("/users/:userId/posts", (req, res) => {
// Use userId from params
// Use page and limit from query
});
4. Combine parameters and query:
// Get a specific comment on a specific post by a specific user
app.get("/users/:userId/posts/:postId/comments/:commentId", (req, res) => {
// Use all three parameters
// Return the comment
});
5. Add filtering to a list endpoint:
// Get all products with filters
app.get("/products", (req, res) => {
// Query parameters: category, minPrice, maxPrice, inStock
// Return filtered products
});
Common Mistakes
Mistake 1: Confusing Parameters and Query
// WRONG - this looks like a parameter but it's a query value
app.get("/users/:userId", (req, res) => {
// What if you want to filter by role?
// You'd have to add another parameter
});
// RIGHT - separate the identifier from the filter
app.get("/users/:userId/posts", (req, res) => {
// userId identifies the user
// use query for filtering: ?sort=date&page=1
});
Mistake 2: Optional Parameters in the Path
// WRONG - optional parameters don't work well in paths
app.get("/users/:userId/:postId", (req, res) => {
// What if someone hits /users/123? postId is missing
});
// RIGHT - use query strings for optional values
app.get("/users/:userId/posts", (req, res) => {
// userId is required
// ?page=2&limit=10 are optional
});
Mistake 3: Too Many Path Parameters
// WRONG - path becomes unreadable
app.get("/a/:b/:c/:d/:e/:f", (req, res) => {
// Hard to understand what this endpoint does
});
// RIGHT - limit path parameters, use query for the rest
app.get("/users/:userId/posts/:postId/comments", (req, res) => {
// Clear hierarchy
// use query for filtering: ?sort=date&page=1
});
Mistake 4: Not Validating Parameter Types
// WRONG - userId could be anything
app.get("/users/:userId", (req, res) => {
const userId = req.params.userId;
// What if userId is "abc"? Should be a number
});
// RIGHT - validate the parameter
app.get("/users/:userId(\\d+)", (req, res) => {
const userId = req.params.userId;
// Now userId is guaranteed to be a number
});
Quick Recap
URL parameters are in the path. They identify a specific resource.
- Example:
/users/123,/posts/42
- Example:
Query strings come after
?. They filter or modify results.- Example:
/users?role=admin,/posts?sort=date
- Example:
Use
req.paramsto access URL parameters.- Example:
req.params.userId
- Example:
Use
req.queryto access query strings.- Example:
req.query.role
- Example:
Use parameters when: The value is essential and identifies a resource.
Use query strings when: The value is optional and filters or modifies results.
Parameters go in order:
/users/:userId/posts/:postIdQuery strings don't need to be in order:
?page=2&sort=nameis the same as?sort=name&page=2Always validate parameter types and check if query parameters exist.
A good API uses both: parameters for hierarchy and identity, query strings for options.
Get the distinction right and your API routes become clear, predictable, and easy to use.
Happy coding! 🚀
If you enjoyed this article, check out my other blogs on this profile. 🔗 Connect with me: LinkedIn | GitHub | X (Twitter)




