DEV Community

Cover image for Simplifying Query Parameters in Next.js 14/15 with React Query and TypeScript
JAKER HOSSAIN
JAKER HOSSAIN

Posted on

1

Simplifying Query Parameters in Next.js 14/15 with React Query and TypeScript

Take control of query parameters using TypeScript-powered utilities like queryExtractor, paramsExtractor, and fetchData. A real-world guide to scaling your dynamic pages in React & Next.js

📌 Overview
If you're building a modern frontend application with Next.js App Router (14/15), you’re likely dealing with query params for:

  1. Search
  2. Filter (price range, brand, category, color)
  3. Sort
  4. Pagination

This article will walk you through how to parse, build, and use query parameters using powerful, reusable TypeScript utilities like:

🔍 paramsExtractor
🔧 queryExtractor
⚙️ fetchData

🧠 Why This Approach?
✅ Next.js 14 compatible — works with App Router and server components
✅ Type-safe and reusable
✅ Easily composable with filters, pagination, and API queries
✅ Works seamlessly with React Query or server-side fetching
✅ Clean and maintainable for long-term scaling

🧩 Example: Category Page in Next.js

const CategoryPage = async ({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[]> | undefined>;
}) => {
  const { filter } = await paramsExtractor({
    searchParam: searchParams,
  });

  const stringifiedQuery = queryExtractor({
    sortBy: filter?.sortBy,
    extra: {
      "variants.sellingPrice[gte]": filter?.min_price,
      "variants.sellingPrice[lte]": filter?.max_price,
      "variants.variantName": filter?.color_name
        ? filter.color_name.split(",").map((b) => b.trim())
        : [],
      "brand.brandName": filter?.brandNames
        ? filter.brandNames.split(",").map((b) => b.trim())
        : [],
      "category.categoryName": filter?.categoryNames
        ? filter.categoryNames.split(",").map((b) => b.trim())
        : [],
    },
  });

  const productsData = await fetchData({
    route: "/product",
    query: stringifiedQuery,
    limit: 18,
    tags: ["/product"],
  });

  // render your component using productsData
};
Enter fullscreen mode Exit fullscreen mode

🧩 What Is paramsExtractor?
If you haven’t read it yet, check out the full breakdown of paramsExtractor. It's a utility that parses searchParams from Next.js and returns a normalized object with:

  1. Cleaned values
  2. Mapped keys
  3. Optional conversion to array, number, or string
  4. Proper defaults

Perfect for server-side usage in dynamic routes like /category?brand=Apple,Samsung&sortBy=price.

🔧 queryExtractor: Build Clean Query Strings
This utility transforms a QueryOptions object into a query string with support for:

  1. Defaults
  2. Nested filtering (e.g., variants.sellingPrice[gte])
  3. Arrays
  4. Optional values

fetchData: Data Fetching Made Clean
fetchData is an abstraction over fetch (or Axios) with a powerful config API:

await fetchData({
  route: "/product",
  query: "category=Phone&brand=Apple",
  limit: 18,
  tags: ["/product"],
});
Enter fullscreen mode Exit fullscreen mode

More on this utility in the official guide 👉 read now

🧵 Connect Everything
With these 3 pieces, your Next.js dynamic page becomes scalable and maintainable:

  1. paramsExtractor: Server-side param parsing
  2. queryExtractor: Build structured queries
  3. fetchData: Fetch paginated, sorted, and filtered data from the backend

All of this while keeping TypeScript and React Query (or fetch) integration seamless.

🔧 Using queryExtractor as a Standalone Utility
📦 Step 1: Add the Utility Function
Here’s the complete code for queryExtractor:

export interface QueryOptions {
  searchTerm?: string;
  sortBy?: string;
  sortOrder?: "asc" | "desc";
  page?: number;
  limit?: number;
  extra?: Record<string, any>;
}

export const queryExtractor = ({
  searchTerm,
  sortBy = "created_at",
  sortOrder = "desc",
  page = 1,
  limit = 10,
  extra = {},
}: QueryOptions): string => {
  const query = new URLSearchParams();

  if (searchTerm?.trim()) query.set("searchTerm", searchTerm.trim());
  query.set("sortBy", sortBy);
  query.set("sortOrder", sortOrder);
  query.set("page", Math.max(1, page).toString());
  query.set("limit", Math.max(1, limit).toString());

  Object.entries(extra).forEach(([key, value]) => {
    if (value !== undefined && value !== null) {
      if (Array.isArray(value)) {
        value.forEach((val) => {
          if (val !== "") query.append(key, String(val));
        });
      } else {
        query.append(key, String(value));
      }
    }
  });

  return query.toString();
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Basic Example

import { queryExtractor } from "./queryExtractor";

const queryString = queryExtractor({
  searchTerm: "laptop",
  sortBy: "price",
  sortOrder: "asc",
  page: 2,
  limit: 20,
  extra: {
    "brand": ["HP", "Dell"],
    "category": "Electronics",
    "price[gte]": 1000,
    "price[lte]": 3000,
  },
});

console.log(queryString);

// Output:
// searchTerm=laptop&sortBy=price&sortOrder=asc&page=2&limit=20&brand=HP&brand=Dell&category=Electronics&price[gte]=1000&price[lte]=3000
Enter fullscreen mode Exit fullscreen mode

💡Pro Tips

  1. Supports arrays (e.g. brand=HP&brand=Dell)
  2. Ignores empty/null/undefined values
  3. Safe defaults for sortBy, sortOrder, page, and limit
  4. Can be reused anywhere: backend services, Node.js CLI tools, etc.

🏁 Final Thoughts
This setup:

✅ Makes query management clean and DRY
✅ Enhances developer experience with strong typing
✅ Scales easily with complex filters
✅ Plays nicely with Next.js App Router
✅ Perfect for dynamic category/product/search pages

👨‍💻 About the Author
Name: JAKER HOSSAIN
Username: @jackfd120
Role: Senior Frontend Developer
Portfolio: https://www.poranfolio.space/

I write about scalable frontend architecture, modern React/Next.js patterns, and TypeScript best practices.
If you liked this post, feel free to follow or reach out via my portfolio!

The Community for IaC Practitioners

The Community for IaC Practitioners

Join IaCConf on August 27 for a virtual event that dives into the security and governance challenges of managing infrastructure as code at scale.

Join us August 27

Top comments (0)

Build the product, not the plumbing—NextJS first

Build the product, not the plumbing—NextJS first

Kinde handles the boring but critical stuff: auth, permissions, and billing—all from one NextJS SDK.

Get a free account

👋 Kindness is contagious

Explore this practical breakdown on DEV’s open platform, where developers from every background come together to push boundaries. No matter your experience, your viewpoint enriches the conversation.

Dropping a simple “thank you” or question in the comments goes a long way in supporting authors—your feedback helps ideas evolve.

At DEV, shared discovery drives progress and builds lasting bonds. If this post resonated, a quick nod of appreciation can make all the difference.

Okay