@marasuchus/next
v0.4.0
Published
Next.js helpers: tables (URLs + pagination), client search-param hooks, form server actions and passwords
Maintainers
Readme
@marasuchus/next
Next.js helpers for list pages, forms and passwords, published as a public package on npmjs.com. ESM only, types included.
Installation
pnpm add @marasuchus/next@marasuchus/domain is always installed (used by ./forms-server).
Peers (next, react, zod, bcryptjs) are optional: install only the
ones required by the modules you use (see table below).
Modules
Each module is imported via subpath, with no barrel loading everything:
import { buildSearchParamsUrl } from "@marasuchus/next/tables";| Subpath | Environment | Required peers |
| ---------------- | ------------ | -------------- |
| ./tables | anywhere | none |
| ./tables-client| Client Comp. | next, react|
| ./forms-server | Server only | zod, next |
| ./password | Server only | bcryptjs |
./tables is pure (no next/* import): safe in Server Components,
Client Components and Node scripts. ./tables-client has "use client":
import it only from Client Components. ./forms-server imports
next/cache + next/navigation, and ./password wraps bcryptjs:
import them only from server code, never from client code.
tables
Query-string helpers for list pages plus the page numbers for the pagination control.
toUrlRecord(params): object of filters toRecord<string, string>, dropping null/undefined/blank values and joining arrays with,.buildSearchParamsUrl(pathname, params):pathname?key=value.createPageHrefBuilder(pathname, baseParams):(page) => href, addingpageonly when > 1.parseOptionalIntParam(s, { min }?): strict integer orundefined(rejects"12abc","1.5","1e3", unsafe integers).getVisiblePages(page, totalPages, { ellipsisThreshold?, siblingCount? }?): keeps the first two and last two pages plus siblings around the current one, collapsing the gap into"ellipsis".
import {
buildSearchParamsUrl,
createPageHrefBuilder,
getVisiblePages,
} from "@marasuchus/next/tables";
buildSearchParamsUrl("/users", { q: "ana", page: 3 }); // "/users?q=ana&page=3"
createPageHrefBuilder("/users", { q: "ana" })(1); // "/users?q=ana"
getVisiblePages(1, 10); // [1, 2, 3, "ellipsis", 9, 10]tables-client
"use client" hook for filter inputs bound to the URL. Updates go through
router.replace inside a transition so typing never blocks the UI.
"use client";
import { useSearchParam } from "@marasuchus/next/tables-client";
const [q, setQuery, isPending] = useSearchParam("q");
// setQuery({ q: "ana", page: "1" }); // null or "" deletes the keyforms-server
Server-only: FormData parsing with a plain zod schema plus
runFormAction, which orchestrates the full server action flow (parse, run
run, revalidate paths, redirect). On parse errors, domain errors, or
exceptions it returns the error object instead of throwing (only the final
redirect throws, as Next requires).
formDataToObject converts FormData to a plain object first (repeated
keys become arrays, user.name / items[0].name expand to nested
structures), so schemas stay plain z.object(...). For JSON-encoded fields
use jsonField, for optional text use z.preprocess(emptyToUndefined, ...),
and for special cases pass a custom toObject mapper to parseForm /
runFormAction.
The Result / DomainError contract comes from @marasuchus/domain/result
(a regular dependency, re-exported here) so the run callback can build
results with a single install.
import { runFormAction } from "@marasuchus/next/forms-server";
export async function createUserAction(formData: FormData) {
return runFormAction({
formData,
schema: CreateUserSchema,
invalidMessage: "Invalid form",
run: async (data) => createUser(data), // => Promise<Result<...>>
revalidate: "/users",
redirectTo: "/users",
});
}password
Server-only bcryptjs wrappers. NOTE: bcrypt only uses the first 72 bytes,
so cap password length in the form schema.
import { hashPassword, verifyPassword } from "@marasuchus/next/password";
const hashed = await hashPassword(password);
await verifyPassword(password, hashed); // trueLicense
MIT — see LICENSE.
