
Nuqs Scaffolder
- 82 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
nuqs-scaffolder is a Claude Code skill for ai & agent building.
About
nuqs-scaffolder is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- nuqs-scaffolder
- AI & Agent Building
- AI-coding skill
Nuqs Scaffolder by the numbers
- 82 all-time installs (skills.sh)
- +7 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,148 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill nuqs-scaffolderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 82 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I helps with ai & agent building tasks during AI-assisted development.?
Helps with ai & agent building tasks during AI-assisted development.
Who is it for?
Best when you're working on ai & agent building and need structured help with nuqs scaffolder.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks during AI-assisted development., or when nuqs-scaffolder is a claude code skill for ai & agent building.
What you get
Structured output aligned to nuqs-scaffolder: nuqs-scaffolder, AI & Agent Building.
Files
nuqs Scaffolder
Generate a coherent set of nuqs files from one spec. The skill is template-driven — you read the spec, copy the templates, and substitute placeholders. No build step, no codegen runtime; the templates ARE the artifact.
When to Apply
Use this skill when:
- A new Next.js page needs URL-backed filters, pagination, search, or sort state
- You're standardising an existing page's ad-hoc
useStatefilters onto nuqs - A code review keeps catching client/server drift in parser definitions — this skill makes drift mechanically impossible because both sides import the same map
- A user asks to "add Standard Schema validation to these query params for tRPC" — the generated
searchParams.server.tsalready exports the schema
If the codebase has legacy nuqs patterns instead, run the `nuqs-codemod-runner` skill first.
How to Use
1. Read or create a spec. Start from assets/templates/spec.template.json and fill in name, module, and params. See "Spec Format" below. 2. Render each template by replacing placeholders with values derived from the spec. 3. Write each rendered file to the path computed from config.json (overridable per-call). 4. Show the user the diff before committing — this skill never modifies existing files; if a target path exists, ask before overwriting.
The agent does the rendering — Claude is the templating engine. Each template is annotated with markers (/*= ... =*/) that name the placeholder slot and document the substitution rule.
Spec Format
{
"name": "Search", // PascalCase — drives the exported symbol names
"module": "search", // kebab-case — drives file paths and the "module" folder
"params": {
"q": { "type": "string", "default": "" },
"page": { "type": "integer", "default": 1 },
"limit": { "type": "integer", "default": 10 },
"categories": { "type": "array-of-string-native", "default": [] },
"sort": { "type": "string-literal", "values": ["asc","desc"], "default": "asc" },
"minPrice": { "type": "float", "default": null },
"lastSeen": { "type": "iso-date", "default": null }
}
}Supported type values
type | Parser used | Notes |
|---|---|---|
string | parseAsString | |
integer | parseAsInteger | |
float | parseAsFloat | |
boolean | parseAsBoolean | |
iso-date | parseAsIsoDate | Date-only |
iso-date-time | parseAsIsoDateTime | Date + time |
timestamp | parseAsTimestamp | ms since epoch |
hex | parseAsHex | Numeric value, hex URL form |
index | parseAsIndex | 0-based in code, 1-based in URL |
array-of-string | parseAsArrayOf(parseAsString) | ?tags=a,b,c |
array-of-string-native | parseAsNativeArrayOf(parseAsString) | ?tag=a&tag=b — requires nuqs ≥ 2.7 |
string-literal | parseAsStringLiteral(values) | Requires values: string[] |
number-literal | parseAsNumberLiteral(values) | Requires values: number[] |
json | parseAsJson(SchemaName.parse) | Generates a Zod schema stub; mark default separately |
If default is null, the param is nullable; otherwise the template uses .withDefault(...).
Available Templates
| Template | Renders to (default) | Loaded when |
|---|---|---|
| `searchParams.ts.template` | lib/{module}-search-params.ts | Always |
| `searchParams.server.ts.template` | lib/{module}-search-params.server.ts | Always |
| `filters.tsx.template` | components/{module}/{name}-filters.tsx | Always |
| `filters.test.tsx.template` | components/{module}/{name}-filters.test.tsx | If config.generate_tests is true |
| `spec.json.template` | Anywhere — starter for the user | First-run prompt |
Template files end in .template so editors don't apply syntax highlighting to placeholder markers — the original extension is preserved as the suffix-before-.template so you can still tell at a glance what the rendered file will be.
Paths are configurable in config.json — override globs, file naming style (kebab vs PascalCase), and whether tests are emitted.
Placeholder Reference
All templates use the same placeholder syntax. The agent substitutes them in one pass:
| Placeholder | Source | Example |
|---|---|---|
__NAME__ | spec.name | Search |
__name__ | camelCase form of spec.name | search |
__module__ | spec.module | search |
/*= PARSERS =*/ | Iterate spec.params → key: parseAsXxx.withDefault(...) lines | see template |
/*= COMPONENT_FIELDS =*/ | Iterate spec.params → one input/select per type | see template |
/*= TEST_CASES =*/ | Iterate spec.params → one assertion per default | see template |
/*= NULLABLE_IMPORTS =*/ | Add Nullable helper import if any param is nullable | conditional |
/*= ZOD_SCHEMAS =*/ | For json type params, emit a Zod schema stub | conditional |
/*= ... =*/ markers are instructions to the agent, not literal substitutions. Replace the entire marker (including the /*= =*/ delimiters) with the expanded content.
Conventions
Read `references/conventions.md` for:
- File naming (kebab-case) and why
- Import ordering (external → nuqs → internal → relative) and why
- Why the server file exists as a sibling, not inside
app/ - When to fork the templates (you usually shouldn't)
Setup
config.json is pre-populated with sensible Next.js App Router defaults. Override only if your repo uses different conventions:
{
"lib_dir": "lib",
"components_dir": "components",
"generate_tests": true,
"test_runner": "vitest"
}On first use, the agent should ask the user for the spec via AskUserQuestion if no spec file is provided.
Related Skills
- `nuqs` — Best-practice reference these templates encode. Read it to understand WHY the templates are shaped this way.
- `nuqs-codemod-runner` — Run BEFORE this skill if migrating an existing page from pre-v2.5 nuqs.
Gotchas
See `gotchas.md` for edge cases discovered during use.
nuqs Scaffolder
This curated skill mirrors SKILL.md. When maintaining it, keep parser maps, server loaders, client components, and tests generated in lockstep from the same URL-state spec.
// __name__-filters.test.tsx — Vitest tests for <__NAME__Filters />.
// Generated by nuqs-scaffolder. Asserts that the URL drives state and vice versa.
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { __NAME__Filters } from './__name__-filters';
function renderWithSearch(searchParams: Record<string, string> = {}) {
return render(
<NuqsTestingAdapter searchParams={searchParams}>
<__NAME__Filters />
</NuqsTestingAdapter>
);
}
describe('<__NAME__Filters />', () => {
it('renders without crashing under NuqsTestingAdapter', () => {
renderWithSearch();
// Smoke test — the form must mount even with no initial params.
expect(screen.getByRole('form')).toBeInTheDocument();
});
/*= TEST_CASES =*/
// For each entry in spec.params, emit ONE `it(...)` block that verifies the
// initial URL value flows into the rendered field. Recipes:
//
// string:
// it('reads __key__ from the URL', () => {
// renderWithSearch({ __key__: 'hello' });
// expect(screen.getByLabelText('__key__')).toHaveValue('hello');
// });
//
// integer / float:
// it('reads __key__ as a number from the URL', () => {
// renderWithSearch({ __key__: '42' });
// expect(screen.getByLabelText('__key__')).toHaveValue(42);
// });
//
// boolean:
// it('reads __key__ as a checkbox state from the URL', () => {
// renderWithSearch({ __key__: 'true' });
// expect(screen.getByLabelText('__key__')).toBeChecked();
// });
//
// string-literal:
// it('reads __key__ as the selected option from the URL', () => {
// renderWithSearch({ __key__: <one of spec.values> });
// expect(screen.getByLabelText('__key__')).toHaveValue(<that value>);
// });
//
// array-of-string-native:
// it('renders chips for each __key__ value in the URL', () => {
// renderWithSearch({ __key__: 'a', __key__: 'b' }); // NuqsTestingAdapter accepts query strings; use those for multi-value keys.
// // ...
// });
//
// Skip json types — assertions are shape-specific; write those by hand.
it('Reset clears every filter back to the default', () => {
renderWithSearch({ /* fill in non-default values for each param */ });
fireEvent.click(screen.getByRole('button', { name: /reset/i }));
// After clicking Reset, every input should show its default value.
});
});
// __name__-filters.tsx — client component for the __NAME__ page.
// Generated by nuqs-scaffolder. Uses the shared parser map from __module__-search-params.ts.
'use client';
import { useQueryStates } from 'nuqs';
import { __name__SearchParams } from '@/__libDir__/__module__-search-params';
/**
* URL-backed filters for the /__module__ page. Edit the spec, not this file —
* field rendering below is generated to match spec.params.
*/
export function __NAME__Filters() {
const [filters, setFilters] = useQueryStates(__name__SearchParams);
return (
<form className="filters" onSubmit={(e) => e.preventDefault()}>
/*= COMPONENT_FIELDS =*/
// For each entry in spec.params, emit ONE field block. Use these recipes:
//
// string:
// <label>
// __key__
// <input
// type="text"
// value={filters.__key__}
// onChange={(e) => setFilters({ __key__: e.target.value })}
// />
// </label>
//
// integer / float / index / hex:
// <label>
// __key__
// <input
// type="number"
// value={filters.__key__ ?? ''}
// onChange={(e) => setFilters({ __key__: e.target.value === '' ? null : Number(e.target.value) })}
// />
// </label>
//
// boolean:
// <label>
// <input
// type="checkbox"
// checked={filters.__key__}
// onChange={(e) => setFilters({ __key__: e.target.checked })}
// />
// __key__
// </label>
//
// iso-date / iso-date-time:
// <label>
// __key__
// <input
// type={spec.type === 'iso-date' ? 'date' : 'datetime-local'}
// value={filters.__key__?.toISOString().slice(0, 10) ?? ''}
// onChange={(e) => setFilters({ __key__: e.target.value === '' ? null : new Date(e.target.value) })}
// />
// </label>
//
// string-literal (renders a <select>):
// <label>
// __key__
// <select
// value={filters.__key__}
// onChange={(e) => setFilters({ __key__: e.target.value as (typeof __name__SearchParams.__key__.values)[number] })}
// >
// {/* one <option> per spec.params.__key__.values */}
// </select>
// </label>
//
// array-of-string / array-of-string-native (multi-select chips):
// <fieldset>
// <legend>__key__</legend>
// {filters.__key__.map((tag) => (
// <button
// key={tag}
// type="button"
// onClick={() =>
// setFilters({ __key__: filters.__key__.filter((t) => t !== tag) })
// }
// >
// {tag} ×
// </button>
// ))}
// {/* an input that pushes onto filters.__key__ when the user submits */}
// </fieldset>
//
// json:
// {/* shape-specific UI — write by hand */}
<button
type="button"
onClick={() => setFilters(null)} // Clears every key back to its default
>
Reset
</button>
</form>
);
}
// __module__-search-params.server.ts — server-side bindings for the __NAME__ page.
// Generated by nuqs-scaffolder. Imports nuqs/server (not nuqs) so the 'use client'
// boundary doesn't get contaminated.
//
// Three exports:
// loadSearchParams — call this in your page() at the top
// __module__SearchParamsCache — use this for nested Server Components that need .get()
// serialize__NAME__ — build canonical URLs (links, redirects, generateMetadata)
//
// The schema export at the bottom plugs straight into tRPC, TanStack Router, or any
// other Standard Schema consumer.
import {
createLoader,
createSearchParamsCache,
createSerializer,
createStandardSchemaV1,
} from 'nuqs/server';
// Re-import the parser map from the shared file. DO NOT redefine — drift kills SSR.
import { __name__SearchParams } from './__module__-search-params';
/**
* Use in your page when you don't need nested Server Components to read params:
*
* export default async function Page({ searchParams }) {
* const params = await loadSearchParams(searchParams);
* return <Results {...params} />;
* }
*/
export const loadSearchParams = createLoader(__name__SearchParams);
/**
* Use when nested Server Components need to .get() params without prop drilling:
*
* export default async function Page({ searchParams }) {
* await __module__SearchParamsCache.parse(searchParams);
* return <NestedTree />; // children call __module__SearchParamsCache.get('q')
* }
*/
export const __module__SearchParamsCache = createSearchParamsCache(__name__SearchParams);
/**
* Build a canonical URL for the /__module__ page. Use in generateMetadata for SEO
* and anywhere you need an SSR-safe link.
*
* const url = serialize__NAME__('/__module__', { q: 'react' });
*/
export const serialize__NAME__ = createSerializer(__name__SearchParams, {
// Stable key order across renders prevents SEO duplicate-URL bugs.
processUrlSearchParams(params) {
params.sort();
return params;
},
});
/**
* Standard Schema export — plug into tRPC, TanStack Router, or any Standard Schema
* consumer that needs to validate the same shape.
*/
export const __name__SearchParamsSchema = createStandardSchemaV1(__name__SearchParams);
// __module__-search-params.ts — shared parser map for the __NAME__ page.
// Generated by nuqs-scaffolder. Edit the spec, don't hand-edit this file —
// drift between this file and __module__-search-params.server.ts breaks SSR/CSR consistency.
//
// Imported from BOTH client (useQueryStates) and server (createSearchParamsCache).
// Safe to import from either side: `nuqs` is client-marked at runtime but the parser
// builders are tree-shakeable pure values.
import {
/*= PARSER_IMPORTS =*/
// Pick the imports from this set based on which `type`s appear in spec.params:
// parseAsString, parseAsInteger, parseAsFloat, parseAsBoolean,
// parseAsIsoDate, parseAsIsoDateTime, parseAsTimestamp,
// parseAsHex, parseAsIndex,
// parseAsArrayOf, parseAsNativeArrayOf,
// parseAsStringLiteral, parseAsNumberLiteral,
// parseAsJson
} from 'nuqs';
/*= ZOD_SCHEMAS =*/
// For each spec.params[key] with type === 'json', emit:
// import { z } from 'zod';
// export const __NAME____key__Schema = z.object({ /* TODO: fill in fields */ });
// Skip this entire block if no params are json-typed.
/**
* Parser map for /__module__ URL state.
*
* Consume on the client with:
* const [state, setState] = useQueryStates(__name__SearchParams);
*
* Consume on the server with:
* await loadSearchParams(searchParams) // see __module__-search-params.server.ts
*/
export const __name__SearchParams = {
/*= PARSERS =*/
// For each entry in spec.params, emit ONE line of the form:
// <key>: <parser-chain>,
//
// Construct <parser-chain> per the `type`:
// string → parseAsString
// integer → parseAsInteger
// float → parseAsFloat
// boolean → parseAsBoolean
// iso-date → parseAsIsoDate
// iso-date-time → parseAsIsoDateTime
// timestamp → parseAsTimestamp
// hex → parseAsHex
// index → parseAsIndex
// array-of-string → parseAsArrayOf(parseAsString)
// array-of-string-native → parseAsNativeArrayOf(parseAsString)
// string-literal → parseAsStringLiteral([...spec.values] as const)
// number-literal → parseAsNumberLiteral([...spec.values] as const)
// json → parseAsJson(<ZodSchema>.parse)
//
// Append `.withDefault(<value>)` ONLY if spec.default is not null.
// For `array-of-*`, render the default as an array literal.
// For `string-literal`, the default must be one of `values`.
} as const;
export type __NAME__SearchParams = {
// Optional: emit a type alias if helpful. Most callers can rely on inferQueryStates<typeof __name__SearchParams>.
};
{
"$comment": "nuqs-scaffolder spec — fill in name, module, and params, then ask the skill to render the four files.",
"name": "__NAME__",
"module": "__module__",
"params": {
"q": { "type": "string", "default": "" },
"page": { "type": "integer", "default": 1 },
"categories": { "type": "array-of-string-native", "default": [] },
"sort": { "type": "string-literal", "values": ["asc","desc"], "default": "asc" }
}
}
{
"lib_dir": "lib",
"components_dir": "components",
"generate_tests": true,
"test_runner": "vitest",
"file_case": "kebab",
"adapter_import": "nuqs/adapters/next/app",
"_setup_instructions": {
"lib_dir": "Directory (relative to repo root) where parser maps go. Default 'lib' fits Next.js App Router.",
"components_dir": "Directory for client filter components. Default 'components'.",
"generate_tests": "Whether to emit a .test.tsx file alongside each component. Set false if the project has no test runner.",
"test_runner": "One of: vitest, jest. Determines test-template syntax and imports.",
"file_case": "Filename case style. 'kebab' = search-filters.tsx (recommended). 'pascal' = SearchFilters.tsx.",
"adapter_import": "Which NuqsAdapter the test wraps. Use 'nuqs/adapters/testing' inside tests; this setting is for the server-side imports."
}
}
Gotchas
Failure modes discovered while using this skill. Append-only, with dates.
---
No known gotchas yet — this is a fresh skill. Add entries here as real-world runs surface edge cases.
Template
### {one-line title of the failure}
{What goes wrong. Be specific — name the spec field, the template, the symptom.}
Fix: {What to do instead, or how to recover.}
Added: {YYYY-MM-DD}{
"version": "1.0.4",
"organization": "Community",
"technology": "nuqs",
"discipline": "extraction",
"type": "scaffolding",
"date": "May 2026",
"abstract": "Template-driven scaffolder for nuqs URL-state filters in Next.js App Router. From a single JSON spec, generates four coherent files — client parser map, server loader/cache/serializer, client component, and Vitest test — all sharing one parser definition map per the nuqs Standard Schema pattern. Encodes the v2.5+ conventions from the companion `nuqs` distillation skill.",
"references": [
"https://nuqs.dev/docs/parsers/built-in",
"https://nuqs.dev/docs/server-side",
"https://nuqs.dev/docs/testing",
"https://nuqs.dev/blog/nuqs-2.5"
]
}
Conventions
The four files this skill generates aren't arbitrary — every choice exists to prevent a specific class of bug we've seen in production nuqs codebases. If you fork a template, read this doc first so you know what protections you're giving up.
1. The shared client-safe parser map lives at lib/{module}-search-params.ts
Why: Both useQueryStates (client) and createSearchParamsCache (server) must reference the same parser objects, not just the same shape. If a client component imports parseAsInteger.withDefault(1) and the server file separately defines parseAsInteger.withDefault(0), the hydration check passes (both are numbers) but the rendered output disagrees by one. The fix is structural: one file, two consumers.
Why this file uses `nuqs` and not `nuqs/server`: The parser builders (parseAsX) are pure values exported from 'nuqs'. The 'use client' marker on 'nuqs' is a runtime marker (it taints components, not constants); pulling parser builders out of 'nuqs' does not turn the file into a Client Component. The corollary: do not put hooks, side effects, or React in this file.
2. The server file is a sibling, not under app/
Why: Files under app/ are router-aware in Next.js. A searchParams.server.ts placed in app/search/ would be picked up as part of the route tree if the framework ever decides a .server.ts segment means something. Keeping it in lib/ future-proofs against that, and matches Next.js's documented "shared utilities live in lib/" pattern.
Why two files instead of one with conditional exports: Next.js doesn't reliably tree-shake 'use client'-marked modules out of Server Component bundles. Splitting the file makes the boundary explicit and the import-time cost zero on the server.
3. File naming is kebab-case
Why: macOS is case-insensitive by default; Linux is case-sensitive. SearchFilters.tsx and searchFilters.tsx resolving to the same file on a developer's Mac but two different files in CI is a class of bug that costs hours to debug. Kebab-case removes the failure mode entirely. The compromise is that the file search-filters.tsx exports <SearchFilters /> — slight redundancy, but the export name follows React conventions.
If your project insists on PascalCase filenames, set config.file_case: "pascal".
4. Imports are grouped: external → nuqs → internal → relative
// 1. External (React, etc.)
import { useState } from 'react';
// 2. nuqs (always its own group — makes drift between client/server bindings visible at review time)
import { useQueryStates } from 'nuqs';
// 3. Internal absolute (anything from `@/...`)
import { searchParams } from '@/lib/search-search-params';
// 4. Relative
import { Pagination } from './pagination';Why nuqs gets its own group: During code review, the line you most want to spot is "is this file importing from nuqs or nuqs/server?" Putting nuqs alone in group 2 makes that single line easy to find. Auto-formatters (Prettier with import-sort) will preserve this grouping if you add a blank line between groups.
5. The component always uses useQueryStates, not individual useQueryState calls
Why: Atomic updates. When a user clicks "Reset", setFilters(null) clears every key in one URL flush. With individual hooks, you'd need to call each setter in sequence — nuqs batches them, but the type-safety win is gone, and a future refactor that introduces a conditional setter will silently break atomicity.
Trade-off: useQueryStates re-renders whenever ANY key changes, even on non-Next.js adapters with key isolation. For a filters panel that's fine — every input lives in this one component. If you later split the panel across multiple components and only care about specific keys, switch those leaf components to individual useQueryState calls (see the perf-key-isolation rule in the companion nuqs skill).
6. The Reset button calls setFilters(null)
Why: Passing null to useQueryStates is the documented way to clear every key back to its parser default. Passing { q: null, page: null, ... } works but adds maintenance burden — every new param requires updating the reset call.
7. The server file always emits all four exports (loadSearchParams, Cache, serialize, Schema)
Why: They're cheap (tree-shakeable pure functions) and removing one is harder than keeping it. Most pages only end up using loadSearchParams and serialize, but having Cache and Schema available for free pays off the first time someone adds a nested Server Component or wires up tRPC.
8. serialize uses processUrlSearchParams: params.sort()
Why: Stable URL key ordering. /search?b=2&a=1 and /search?a=1&b=2 are different cache keys to Google, browsers, and CDNs even though they render identically. Sorting keys before serialisation eliminates that source of duplicate URLs. The cost is purely cosmetic (the URL looks less "natural" to humans) — worth it.
Counter-case: If your URLs are user-facing and you care about typing order (e.g., a builder UI where the URL "remembers" the order the user added filters), set processUrlSearchParams: (p) => p to disable sorting.
9. Tests use NuqsTestingAdapter, not the real Next.js adapter
Why: NuqsTestingAdapter takes searchParams as a prop, so each test starts from a known URL state. The real nuqs/adapters/next/app relies on the Next.js router context — testing against it requires either Playwright or a Next.js-aware mock, both heavier than necessary for unit tests of filter logic.
The trade-off: tests pass searchParams as a Record<string, string> or query string, so multi-value keys (array-of-string-native) need the query-string form: ?tag=a&tag=b.
When to fork the templates
Almost never. The conventions above each protect against a specific bug class; removing one usually re-opens that bug class. Reasonable forks:
- Different test runner — set
config.test_runner: "jest"and adjust the test template's imports. - `generateMetadata` already lives elsewhere — drop
serialize__NAME__from the server file. - Project uses module-scoped CSS or Tailwind classes — extend the component template's JSX; the form-structure choices above still apply.
Don't fork to:
- Move parser definitions into the component file (defeats client/server sharing).
- Use individual
useQueryStatecalls for "performance" (filters panels don't need it). - Drop
processUrlSearchParams: params.sort()"for now" (SEO bills come due).
Related skills
FAQ
What does nuqs-scaffolder do?
nuqs-scaffolder is a Claude Code skill for ai & agent building.
When should I use nuqs-scaffolder?
When you need to helps with ai & agent building tasks during AI-assisted development., or when nuqs-scaffolder is a claude code skill for ai & agent building.
What are the main capabilities?
nuqs-scaffolder; AI & Agent Building; AI-coding skill.