DEV Community

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

Posted on

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!

Top comments (0)