@zvk/next-kit
v0.2.6
Published
Next App Router adapters for server actions, cache invalidation, form data, and route handlers.
Readme
@zvk/next-kit
Next App Router adapters for server actions, cache invalidation, FormData parsing, and route handlers.
import { formDataToObject } from "@zvk/next-kit";
import {
createFormDataMutationAction,
createMappedNextMutationAction,
createNextMutationAction,
createZodNextMutationAction,
parseFormDataActionInput,
parseZodFormDataActionInput,
createPassthroughNextMutationAction,
createPassthroughNextMutationActionFactory,
createZodFormDataMutationAction,
createZodPassthroughNextMutationAction,
createZodPassthroughNextMutationActionFactory,
type ParseFormDataActionInput,
type ParseZodFormDataActionInput,
} from "@zvk/next-kit/actions";
import { createNextRouteHandler, jsonErrorResponse, parseJsonRequestBody, parseSearchParams, parseZodJsonRequestBody, parseZodSearchParams, readJsonRequestBody, readRawRequestBody, readRequestBody } from "@zvk/next-kit/route-handlers";
import { captureRedirectPath, createJsonRequest, createMutationActionScenario, createNextNavigationRedirectMock, createRouteHandlerContext } from "@zvk/next-kit/test-utils";
import { buildAppUrl, safeNextPath } from "@zvk/next-kit/url";The package keeps its runtime helpers narrow. It depends on @zvk/contracts, @zvk/feature-kit, @zvk/server-kit, server-only, and a Next 15 || 16 peer, but it does not add React components, React Query, Supabase, Drizzle, or app-specific policy.
See ../../docs/package-boundary-matrix.md for the package-family runtime
boundary matrix. The actions, cache, and route-handlers subpaths are
server-only; the root export stays browser-safe.
@zvk/next-kit/actions intentionally keeps both layers of action helpers. Use
createNextMutationAction when you want the canonical contract ActionResult
wrapper from @zvk/contracts, and use the mapped or passthrough helpers when
the app owns the flattened result envelope or needs app-owned wrapper output.
Use createZodNextMutationAction for the same canonical ActionResult shape
when the app already owns a Zod-like schema and should not compose a schema
adapter locally.
Use createFormDataMutationAction or createZodFormDataMutationAction for
ordinary <form action> mutations that should parse FormData, return a typed
ActionResult, map validation and dependency failures to ContractError, run
the mutation, revalidate only after success, and also expose a void
formAction for React form props.
For canonical mutation helpers, pass shouldRevalidate with revalidate when
successful mutations can return a no-op result that should not invalidate cache
targets.
Use @zvk/next-kit/cache for path and tag cache invalidation in Next App
Router server code. Path targets call revalidatePath; tag targets call
revalidateTag. The package expects the Next 15 or 16 next/cache APIs.
Use @zvk/next-kit/url for framework-safe URL construction, next-path
sanitization, and search-param appending. Redirect destinations and route names
remain application policy.
safeNextPath accepts unknown external values, including form-derived values,
and falls back unless the input is a same-origin local path.
Use createMappedNextMutationAction when an application owns a flattened action
result envelope and needs to map shared contract success/error results into that
shape.
Use parseFormDataActionInput when a redirecting or custom server action needs
to convert FormData and validate it through a schema adapter while keeping
redirect destinations, auth policy, and product copy in the app.
Use parseZodFormDataActionInput when the app already validates with Zod-like
schemas and should not compose the schema adapter locally.
Use ParseFormDataActionInput or ParseZodFormDataActionInput for app-local
wrappers that preserve package parser input typing while adding app-owned
redirect policy or response mapping.
Pass values for server-owned fields, such as organization IDs or fixed return
paths, that must override any submitted form values before validation.
Use createPassthroughNextMutationAction when run already returns the
application-owned envelope and package-owned validation, authorization,
dependency, or revalidation failures should be mapped into that same envelope.
Use createZodPassthroughNextMutationAction when the caller already owns a
Zod-like schema and should avoid local schema-adapter glue.
When passing revalidate, also pass shouldRevalidate so app-level error
envelopes do not accidentally invalidate cache targets.
Use createPassthroughNextMutationActionFactory when an app already owns schema
adapters and wants to bind its package-error mapper and default app-envelope
revalidation predicate once.
Use createZodPassthroughNextMutationActionFactory when an app wants to bind
its package-error mapper and default revalidation predicate once, then create
many app-envelope actions without repeating that wiring.
const createAppMutation = createPassthroughNextMutationActionFactory<
AppActionResult<Project>,
AppActionResult<Project>
>({
mapError: actionErrorFromContractError,
shouldRevalidate: (result) => result.status === "ok",
});
export const saveProject = createAppMutation({
schema: projectInputSchemaAdapter,
getContext: requireProjectContext,
run: saveProjectInput,
revalidate: ["/projects"],
});Keep public action envelopes, redirect targets, auth/session policy, product copy, and route-specific revalidation predicates in the application wrapper.
Use readRequestBody when a custom Route Handler needs content-type-aware
JSON, text, form, or empty body parsing while receiving malformed or unsupported
body failures as contract validation errors. Use parseJsonRequestBody for
streaming or custom-response Route Handlers that
cannot use createNextRouteHandler but still need shared JSON parsing and
schema validation before returning an app-owned response. Use
parseZodJsonRequestBody when the route already owns a Zod-like schema. Use
readJsonRequestBody only when the route needs raw body control before
app-owned validation.
Use parseSearchParams or parseZodSearchParams to normalize
URLSearchParams, preserve repeated keys as arrays, and validate query input
without repeating route-local conversion code. Use readRawRequestBody for
signed webhook routes that need the exact text payload before app-owned
signature verification.
Use jsonResponse, jsonErrorResponse, and jsonResultResponse for small
custom JSON Route Handler responses. Pass mapper functions for app-owned
envelopes, product copy, and error details; the package only owns response
serialization, status, and headers.
Use createJsonRequest from @zvk/next-kit/test-utils for App Router tests
that need deterministic JSON Request objects, including raw malformed bodies.
Use createRouteHandlerContext for typed { params } objects and
createMutationActionScenario when action tests need deterministic context,
authorization, run output, and call history without recreating mocks.
Use createNextNavigationRedirectMock and captureRedirectPath when testing
redirecting actions that import next/navigation. They let tests mock redirects
without adding a dependency on a specific test runner. Use
createRedirectTestDouble only when the redirect function is injected directly.
Consumer Recipe Map
These recipes describe how a Next App Router application should combine
@zvk/next-kit with @zvk/ui and @zvk/composite. They are app-side recipes,
not @zvk/next-kit dependencies. Keep React components, auth providers,
billing providers, storage clients, AI SDK transport, table engines, and
product copy in the consuming app.
Server Form Surface
Use createZodFormDataMutationAction or createFormDataMutationAction for the
server action, and render the form with UI/composite packages from app code:
import { createZodFormDataMutationAction } from "@zvk/next-kit/actions";
import { FormSurface } from "@zvk/composite/form-surface";
import { Alert } from "@zvk/ui/alert";
import { Button } from "@zvk/ui/button";
import { Checkbox } from "@zvk/ui/checkbox";
import { Input } from "@zvk/ui/input";
import { NativeSelect } from "@zvk/ui/native-select";
import { Textarea } from "@zvk/ui/textarea";
const saveProject = createZodFormDataMutationAction({
schema: projectFormSchema,
getContext: requireWorkspaceContext,
authorize: canEditProjects,
run: saveProjectSettings,
values: { workspaceId },
revalidate: ["/projects", { tag: "projects" }]
});
export function ProjectSettingsForm({ error }: { readonly error?: string }) {
return (
<FormSurface
action={saveProject.formAction}
alert={error ? <Alert tone="destructive" title={error} /> : null}
footer={<Button type="submit">Save project</Button>}
title="Project settings"
>
<Input label="Project name" name="name" required />
<NativeSelect label="Visibility" name="visibility" items={visibilityOptions} />
<Textarea label="Notes" name="notes" />
<Checkbox label="Enable weekly summary" name="weeklySummary" value="on" />
</FormSurface>
);
}The package parses FormData, maps expected validation/dependency failures, and
revalidates after success. The app still owns auth/session lookup, permissions,
mutation implementation, redirects, toasts, analytics, and user-facing copy.
Route Handler Envelope
Use route-handler helpers around app-owned context, authorization, and service calls. Keep rate limits, persistence, provider clients, and product envelopes in the route or service layer:
import { jsonErrorResponse, jsonResultResponse, parseZodJsonRequestBody } from "@zvk/next-kit/route-handlers";
export async function POST(request: Request): Promise<Response> {
const parsed = await parseZodJsonRequestBody({
request,
schema: createProjectRequestSchema,
invalidMessage: "Project request is invalid"
});
if (!parsed.ok) {
return jsonErrorResponse(
{ status: 400, message: parsed.error.message, code: parsed.error.code },
(error) => ({ status: "error", message: error.message, code: error.code })
);
}
const context = await requireWorkspaceContext(request);
const result = await createProjectFromRequest(parsed.data, context);
return jsonResultResponse(result, {
mapOk: (data) => ({ status: "ok", data }),
mapError: (error) => ({ status: "error", message: error.message, code: error.code }),
okStatus: 201
});
}For routes that fit the shared handler shape, use createNextRouteHandler.
For streaming, multipart, custom status handling, or provider-specific errors,
use parseJsonRequestBody, parseZodJsonRequestBody, readRequestBody,
jsonResponse, jsonErrorResponse, and jsonResultResponse directly.
File Upload Surface
Compose upload UI from @zvk/ui/file-dropzone,
@zvk/ui/file-upload-input, and
@zvk/composite/upload-manager-surface. Use @zvk/next-kit/actions or
@zvk/next-kit/route-handlers only for the app-owned upload flow:
- UI owns file selection, drag/drop affordances, progress rows, errors, and retry/delete button placement.
- The app owns storage provider choice, signed URL creation, object keys, metadata persistence, file-type and size policy, malware scanning, retries, deletion, and audit events.
- Supabase-specific apps can use
@zvk/supabase-kitin the app layer without making UI or composite packages depend on Supabase.
AI Chat Workbench
Compose chat and review surfaces from @zvk/ui/conversation,
@zvk/ui/tool-call-status, @zvk/ui/source-reference-list,
@zvk/ui/diff-viewer, @zvk/composite/conversation-directory,
@zvk/composite/context-selection-basket,
@zvk/composite/sticky-composer,
@zvk/composite/provider-model-selector,
@zvk/composite/generated-artifact-workbench, and
@zvk/composite/review-decision-card.
The app owns AI SDK transport, streaming, model catalogs, prompt assembly,
tool execution, context retrieval, citations, persistence, usage limits,
and error recovery. @zvk/next-kit can help route handlers parse JSON and
return envelopes, but it should not depend on an AI SDK.
Data Table And Index Page
Use @zvk/composite/data-table-page-frame and
@zvk/composite/data-table-control-bar for layout, then place
@zvk/ui/table, @zvk/ui/pagination, @zvk/ui/empty-state,
@zvk/ui/input, and @zvk/ui/applied-filter-list inside the app-owned slots.
Keep row models, sorting, filtering, query state, URL sync, pagination, selection state, column visibility, saved views, loading, and bulk mutations in the consuming app. ZVK packages provide semantic markup and placement, not a table engine.
Auth, Billing, And Access Frames
Use @zvk/composite/account-access-shell,
@zvk/composite/access-state-panel, @zvk/composite/option-comparison-grid,
@zvk/composite/form-surface, @zvk/ui/alert, @zvk/ui/badge, and
@zvk/ui/button for access and plan presentation.
The app owns auth providers, sessions, permissions, plan models, checkout or
customer-portal links, premium entitlement, StoreKit or billing SDK policy, and
audit events. Do not move those policies into @zvk/composite or
@zvk/next-kit.
Root App Setup And Theme Scope
New Next starters should import package styles once at the app root, scope the rendered app, and bind app-owned fonts into ZVK font tokens:
import type * as React from "react";
import "@zvk/ui/styles.css";
import "@zvk/themes/styles.css";
import "@zvk/composite/styles.css";
export default function RootLayout({ children }: { readonly children: React.ReactNode }) {
return (
<html lang="en">
<body>
<div
className="zvk-ui-root"
data-zvk-ui-density="default"
data-zvk-ui-preset="zinc"
data-zvk-ui-theme="light"
style={{
"--zvk-ui-font-family-sans": "var(--font-app-sans)",
"--zvk-ui-font-family-mono": "var(--font-app-mono)"
} as React.CSSProperties}
>
{children}
</div>
</body>
</html>
);
}Verify root setup with a root-scope check, computed font check, desktop/mobile screenshot review, and an app-local CSS budget review before adding custom layout CSS.
Repo Skill
Use .codex/skills/use-zvk-next-kit/SKILL.md when maintaining this package.
See ../../docs/package-boundary-matrix.md for the browser-safe and server-only subpaths.
