javascript 53 lines · 10 steps

Parsing search params in a Next.js page

A Next.js server component sanitizes URL query params into a typed filter object before querying products.

Explained by highlit
1import { notFound } from 'next/navigation';
2 
3const PER_PAGE = 24;
4const SORT_OPTIONS = ['relevance', 'price-asc', 'price-desc', 'newest'];
5 
6function parseFilters(searchParams) {
7 const q = typeof searchParams.q === 'string' ? searchParams.q.trim() : '';
8 
9 const sort = SORT_OPTIONS.includes(searchParams.sort)
10 ? searchParams.sort
11 : 'relevance';
12 
13 const page = Math.max(1, Number.parseInt(searchParams.page, 10) || 1);
14 
15 const min = Number.parseFloat(searchParams.min);
16 const max = Number.parseFloat(searchParams.max);
17 const minPrice = Number.isFinite(min) && min >= 0 ? min : null;
18 const maxPrice = Number.isFinite(max) && max >= 0 ? max : null;
19 
20 const categories = []
21 .concat(searchParams.category ?? [])
22 .filter((c) => typeof c === 'string' && c.length > 0);
23 
24 const inStock = searchParams.inStock === 'true';
25 
26 return { q, sort, page, minPrice, maxPrice, categories, inStock };
27}
28 
29export default async function ProductsPage({ searchParams }) {
30 const filters = parseFilters(await searchParams);
31 
32 if (filters.minPrice !== null && filters.maxPrice !== null && filters.minPrice > filters.maxPrice) {
33 notFound();
34 }
35 
36 const { products, total } = await searchProducts({
37 ...filters,
38 limit: PER_PAGE,
39 offset: (filters.page - 1) * PER_PAGE,
40 });
41 
42 if (filters.page > 1 && products.length === 0) {
43 notFound();
44 }
45 
46 return (
47 <ProductGrid
48 products={products}
49 filters={filters}
50 totalPages={Math.ceil(total / PER_PAGE)}
51 />
52 );
53}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1User-supplied query params are untrusted strings, so coerce and bound each one before using it.
  2. 2Centralizing parsing in one function gives every downstream consumer a clean, predictable filter shape.
  3. 3Returning notFound() for impossible or empty states keeps invalid URLs from rendering broken pages.

Related explainers

javascript
const ROLE_PERMISSIONS = {
  admin: ['users:read', 'users:write', 'billing:read', 'billing:write'],
  manager: ['users:read', 'billing:read'],
  member: ['users:read'],

Role-based permissions middleware in Express

authorization middleware rbac
Intermediate 9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) {
  const formatter = new Intl.NumberFormat(locale);
  const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ',';
  const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';

Live thousand separators without losing the caret

dom intl caret-preservation
Advanced 8 steps
javascript
import { useReducer, useEffect } from "react";
 
const initialState = { status: "idle", data: null, error: null };
 

Building a data-fetching hook in React

custom-hooks usereducer data-fetching
Intermediate 9 steps
ruby
module Paginatable
  extend ActiveSupport::Concern
 
  private

A reusable pagination concern in Rails

pagination concerns http-headers
Intermediate 8 steps
javascript
const express = require('express');
const app = express();
 
app.get('/health', (req, res) => res.json({ status: 'ok' }));

Graceful shutdown in an Express server

graceful-shutdown signal-handling connection-tracking
Advanced 9 steps
go
func UploadChunk(c *gin.Context) {
	uploadID := c.Param("uploadID")
	if !validUploadID.MatchString(uploadID) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid upload id"})

Resumable chunked uploads in Gin

file-upload content-range streaming
Advanced 9 steps

Share this explainer

Here's the card — post it anywhere.

Parsing search params in a Next.js page — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code