CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-nuqs

Type-safe search params state manager for Next.js - Like React.useState, but stored in the URL query string

Overall
score

96%

Overview
Eval results
Files

server-cache.mddocs/

Server-Side Cache

Server-side utilities for accessing and parsing query parameters in React Server Components, providing type-safe query parameter handling with request-scoped caching.

Capabilities

createSearchParamsCache

Creates a request-scoped cache for parsing and accessing query parameters in server components.

/**
 * Creates a search params cache for server-side query parameter parsing
 * @param parsers - Object mapping keys to their respective parsers
 * @returns Cache instance with parse, get, and all methods
 */
function createSearchParamsCache<Parsers extends Record<string, ParserBuilder<any>>>(
  parsers: Parsers
): SearchParamsCache<Parsers>;

interface SearchParamsCache<Parsers extends Record<string, ParserBuilder<any>>> {
  /** Parse the incoming searchParams page prop using the parsers provided */
  parse(searchParams: SearchParams): ParsedSearchParams<Parsers>;
  parse(searchParams: Promise<SearchParams>): Promise<ParsedSearchParams<Parsers>>;
  
  /** Get a single parsed value by key */
  get<Key extends keyof Parsers>(key: Key): ParsedSearchParams<Parsers>[Key];
  
  /** Get all parsed values */
  all(): ParsedSearchParams<Parsers>;
}

type SearchParams = Record<string, string | string[] | undefined>;

type ParsedSearchParams<Parsers extends Record<string, ParserBuilder<any>>> = {
  readonly [K in keyof Parsers]: inferParserType<Parsers[K]>;
};

type inferParserType<Input> =
  Input extends ParserBuilder<any>
    ? Input extends ParserBuilder<any> & { defaultValue: infer Type }
      ? Type
      : ReturnType<Input['parse']> | null
    : never;

Usage Examples:

// app/search/page.tsx
import { createSearchParamsCache, parseAsString, parseAsInteger, parseAsBoolean } from "nuqs/server";

// Define the search params schema
const searchParamsCache = createSearchParamsCache({
  q: parseAsString,
  page: parseAsInteger.withDefault(1),
  category: parseAsString,
  featured: parseAsBoolean.withDefault(false)
});

export default async function SearchPage({ 
  searchParams 
}: { 
  searchParams: Promise<{ [key: string]: string | string[] | undefined }> 
}) {
  // Parse all query parameters (Next.js 15+ with Promise)
  const { q, page, category, featured } = await searchParamsCache.parse(searchParams);
  
  // Types are fully inferred:
  // q: string | null
  // page: number (always defined due to default)
  // category: string | null  
  // featured: boolean (always defined due to default)

  return (
    <div>
      <h1>Search Results</h1>
      {q && <p>Searching for: {q}</p>}
      <p>Page: {page}</p>
      {category && <p>Category: {category}</p>}
      <p>Featured only: {featured}</p>
    </div>
  );
}

// For Next.js 14 and earlier (synchronous searchParams)
export default function SearchPage({ 
  searchParams 
}: { 
  searchParams: { [key: string]: string | string[] | undefined } 
}) {
  const { q, page, category, featured } = searchParamsCache.parse(searchParams);
  // ... rest of component
}

Individual Parameter Access

Access individual parsed parameters without parsing the entire object.

/**
 * Get a single parsed value by key
 * @param key - The key to retrieve from the cache
 * @throws Error if parse() hasn't been called first or key doesn't exist
 */
get<Key extends keyof Parsers>(key: Key): ParsedSearchParams<Parsers>[Key];

/**
 * Get all parsed values from the cache
 * @throws Error if parse() hasn't been called first
 */
all(): ParsedSearchParams<Parsers>;

Usage Examples:

// In a server component after calling parse()
export default async function ProductPage({ searchParams }: { searchParams: Promise<any> }) {
  // Parse all parameters first
  await searchParamsCache.parse(searchParams);
  
  // Access individual values in child components
  return (
    <div>
      <SearchFilters />
      <ProductList />
    </div>
  );
}

// Child server component can access individual values
function SearchFilters() {
  const query = searchParamsCache.get('q');
  const category = searchParamsCache.get('category');
  
  return (
    <div>
      <p>Current search: {query}</p>
      <p>Category: {category}</p>
    </div>
  );
}

// Or access all values
function DebugInfo() {
  const allParams = searchParamsCache.all();
  return <pre>{JSON.stringify(allParams, null, 2)}</pre>;
}

generateMetadata Integration

Use the cache in generateMetadata functions for SEO and page metadata.

import { Metadata } from "next";

export async function generateMetadata({ 
  searchParams 
}: { 
  searchParams: Promise<any> 
}): Promise<Metadata> {
  const { q, category } = await searchParamsCache.parse(searchParams);
  
  const title = q 
    ? `Search results for "${q}"${category ? ` in ${category}` : ''}`
    : 'Search';

  return {
    title,
    description: `Find products${category ? ` in ${category}` : ''}`
  };
}

Advanced Parser Configuration

The cache works with all parser types and configurations.

import { 
  createSearchParamsCache, 
  parseAsStringEnum, 
  parseAsArrayOf, 
  parseAsJson 
} from "nuqs/server";

// Complex parser configuration
const advancedCache = createSearchParamsCache({
  // Enum with default
  status: parseAsStringEnum(['draft', 'published', 'archived'] as const)
    .withDefault('published'),
    
  // Array of strings
  tags: parseAsArrayOf(parseAsString),
  
  // JSON object
  filters: parseAsJson<{
    minPrice: number;
    maxPrice: number;
    brand: string;
  }>(),
  
  // Date range
  startDate: parseAsIsoDateTime,
  endDate: parseAsIsoDateTime,
});

export default async function AdvancedSearchPage({ 
  searchParams 
}: { 
  searchParams: Promise<any> 
}) {
  const params = await advancedCache.parse(searchParams);
  
  // All types are properly inferred:
  // params.status: 'draft' | 'published' | 'archived'
  // params.tags: string[] | null
  // params.filters: { minPrice: number; maxPrice: number; brand: string } | null
  // params.startDate: Date | null
  // params.endDate: Date | null
}

Request Scoping and Caching

The cache is automatically scoped to each request using React's cache function.

// The same cache instance can be used across multiple server components
// in the same request without re-parsing

// layout.tsx
export default async function Layout({ 
  searchParams 
}: { 
  searchParams: Promise<any> 
}) {
  await searchParamsCache.parse(searchParams);
  
  return (
    <div>
      <Header />
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}

// components/Header.tsx
function Header() {
  const query = searchParamsCache.get('q'); // Uses cached value
  return <SearchInput defaultValue={query} />;
}

// components/Sidebar.tsx  
function Sidebar() {
  const category = searchParamsCache.get('category'); // Uses cached value
  return <CategoryFilter selected={category} />;
}

Error Handling

The cache includes built-in error handling for common scenarios.

// Error cases:
try {
  // Throws if parse() hasn't been called first
  const value = searchParamsCache.get('q');
} catch (error) {
  console.error('Must call parse() before accessing values');
}

try {
  // Throws if key doesn't exist in parser schema
  const value = searchParamsCache.get('nonexistent');
} catch (error) {
  console.error('Key not found in parser schema');
}

// Invalid query values are gracefully parsed as null
const params = await searchParamsCache.parse({
  page: 'invalid-number', // Will be parsed as null
  enabled: 'maybe'        // Will be parsed as null (not 'true'/'false')
});

Install with Tessl CLI

npx tessl i tessl/npm-nuqs

docs

index.md

parsers.md

query-states.md

serialization.md

server-cache.md

tile.json