agentsclimarketplace

Nextjs module

Skill Deadlymind/nanolama/skills/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

Install
npx -y skills add Deadlymind/nanolama --skill nextjs-module

Assembled 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/useMutation wrapping the fetchers; components import these.
  • pages — App Router Server/Client Components render hook state, no fetch inline.

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, plus X-CSRFToken on every unsafe method (POST/PUT/PATCH/DELETE). A 403 on a write means that header is missing — never @csrf_exempt the endpoint to silence it. See cookie-auth-csrf for why cookie auth is CSRF-exposed, the cookie flags, credentialed CORS, and refresh/logout.
  • Never hard-code Content-Type: application/json unconditionally. For a FormData body you must omit the header entirely so the browser can set multipart/form-data with 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 in react-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 fetch inside components or pages; that scatters auth/tenant/error logic the client exists to centralize.
  • Return undefined for 204 No Content before calling res.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 (ties rbac-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-csrf
  • react-query
  • rbac-permissions
  • zod-forms
  • drf-zod-contract
  • i18n-rtl

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.