Nextjs module
Structures a Next.js 16 App Router frontend feature as layered modules (lib/api typed fetchers, query-keys, TanStack Query hooks, React 19 pages) fed by one shared fetch client that sends the auth cookie with credentials include, attaches the tenant header, and normalizes errors. Use when scaffolding a new frontend feature or module, building a fetch/apiClient wrapper, wiring cookie-JWT calls to the DRF backend, deciding where fetchers vs hooks vs pages live, or fixing a feature whose data layer is tangled. Not for the cookie-JWT plus CSRF auth seam itself — cookie flags, credentialed CORS, refresh rotation and logout revocation (see cookie-auth-csrf) — query cache config and useMutation invalidation depth (see react-query), form state and validation (see zod-forms), or designing the workflow and screens a module implements (see product-ux-design and visual-ui-design).From its SKILL.md
npx -y skills add Deadlymind/nanolama --skill nextjs-moduleAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.5 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it
Next.js module (layered feature + shared fetch client)
When to use
Starting or reorganizing a frontend feature that talks to the DRF backend, or building the fetch client every feature shares. Keep each feature a thin stack of layers so pages stay declarative and the auth/tenant/error concerns live in one place.
Pattern
One feature = four layers, each depending only on the one below it:
app/(pages) -> hooks (useQuery/useMutation) -> query-keys -> lib/api (typed fetchers)
|
shared fetch client (cookie + tenant + errors)
lib/api/<feature>.ts— typed async fetchers, the only place a URL/verb appears.- query-keys — one factory per feature so hooks and invalidation agree on keys.
- hooks —
useQuery/useMutationwrapping the fetchers; components import these. - pages — App Router Server/Client Components render hook state, no
fetchinline.
Every fetcher goes through one shared client that sends the HttpOnly auth cookie
(credentials: "include"), attaches the tenant header, and throws a normalized error.
One vertical slice, top of stack unchanged from the client up:
// lib/api/client.ts — one shared client: cookie-JWT, tenant header, normalized errors
export class ApiError extends Error {
constructor(public status: number, public detail: unknown) { super(`HTTP ${status}`); }
}
const UNSAFE = /^(POST|PUT|PATCH|DELETE)$/i;
const csrfToken = () =>
decodeURIComponent(document.cookie.match(/(?:^|;\s*)csrftoken=([^;]*)/)?.[1] ?? "");
export async function apiClient<T>(path: string, init: RequestInit = {}): Promise<T> {
const method = init.method ?? "GET";
const isForm = init.body instanceof FormData;
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${path}`, {
...init,
credentials: "include", // HttpOnly cookie rides along; never read the token in JS
headers: {
...(isForm ? {} : { "Content-Type": "application/json" }), // let the browser set the multipart boundary
...(UNSAFE.test(method) ? { "X-CSRFToken": csrfToken() } : {}), // cookie auth => CSRF-exposed
"X-Entreprise": getTenantId(),
...init.headers,
},
});
if (!res.ok) throw new ApiError(res.status, await res.json().catch(() => null));
return res.status === 204 ? (undefined as T) : res.json(); // empty body on 204/DELETE
}
// lib/api/invoices.ts — typed fetchers are the only place a URL/verb appears
export const getInvoices = () => apiClient<Invoice[]>("/api/invoices/");
// query-keys — one factory so hooks and invalidation agree on keys; tenant is IN the key
export const invoiceKeys = {
all: (t: string) => ["tenant", t, "invoices"] as const,
list: (t: string) => [...invoiceKeys.all(t), "list"] as const,
};
// hooks — components import these, never the fetchers
export const useInvoices = () => {
const tenantId = useUI((s) => s.activeEntrepriseId);
return useQuery({
queryKey: invoiceKeys.list(tenantId), // never share a cache entry across tenants
queryFn: getInvoices,
enabled: !!tenantId,
});
};
// app/invoices/page.tsx — declarative page reads hook state
"use client";
export default function InvoicesPage() {
const { data, isPending, error } = useInvoices(); // TanStack v5: isPending, not isLoading
if (isPending) return <Spinner />;
if (error) return <ErrorState error={error} />; // ApiError carries .status
return <InvoiceTable rows={data} />;
}
Adapt to your repo
Rename the tenant header (X-Entreprise) and getTenantId() to match your backend
and where the id lives (often a Zustand store — see the state pattern). Set
NEXT_PUBLIC_API_URL per environment. Pick your folder convention (app/ vs a
features/ directory) and hold it repo-wide. If you server-fetch in a Server
Component, forward the incoming cookie header explicitly — credentials: "include"
only applies to browser fetches.
Gotchas
- The client's auth contract, in one rule:
credentials: "include"on every request, plusX-CSRFTokenon every unsafe method (POST/PUT/PATCH/DELETE). A 403 on a write means that header is missing — never@csrf_exemptthe endpoint to silence it. Seecookie-auth-csrffor why cookie auth is CSRF-exposed, the cookie flags, credentialed CORS, and refresh/logout. - Never hard-code
Content-Type: application/jsonunconditionally. For aFormDatabody you must omit the header entirely so the browser can setmultipart/form-datawith its generated boundary — setting it by hand produces a body the server cannot parse. - Security-critical: the tenant belongs IN the query key, not only in the request
header. Otherwise two tenants share one cache entry and a tenant switch serves the
previous tenant's rows. Scope keys under
["tenant", tenantId, ...]and follow the switch procedure inreact-query. Client-side isolation is defence-in-depth, not the boundary — the server still enforces it. - A silent 401 that only happens in production is almost never your fetcher — it is the
cookie/CORS seam (
cookie-auth-csrf). Check there before editing this layer. - Don't call
fetchinside components or pages; that scatters auth/tenant/error logic the client exists to centralize. - Return
undefinedfor204 No Contentbefore callingres.json(), or a DELETE mutation throws on an empty body. - Render loading, empty, AND error as three distinct states, not just success. A
successfully-fetched empty list (
data.length === 0) is not an error and not still loading — it needs its own empty-state UI (a message plus a call to action, e.g. a "Create your first invoice" button), never an infinite spinner or a blank table. So the page reads:isPending-> spinner,error-> error state, empty -> empty state, else -> the list. - Validate response shapes at the fetcher boundary with Zod when the payload is
untrusted or drifts (see
drf-zod-contract). - Security-critical: do authentication and tenant/authorization checks on the
server — in a Server Component, route handler, or middleware — never in a Client
Component. Anything a
"use client"component can read is attacker-visible, so client-side gating is UX only, not a boundary (tiesrbac-permissions). Keep"use client"at interactive leaves; never put secrets or tenant-authorization logic in client code. For a privileged operation, prefer a Server Action or a server-side fetch over exposing the call (and any check guarding it) to the client.
See also
cookie-auth-csrfreact-queryrbac-permissionszod-formsdrf-zod-contracti18n-rtl
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.