# 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…

- **Type:** Skill
- **Install:** `agentstack add skill-deadlymind-nanolama-nextjs-module`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Deadlymind](https://agentstack.voostack.com/s/deadlymind)
- **Installs:** 0
- **Category:** [Security](https://agentstack.voostack.com/c/security)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Deadlymind](https://github.com/Deadlymind)
- **Source:** https://github.com/Deadlymind/nanolama/tree/main/skills/nextjs-module

## Install

```sh
agentstack add skill-deadlymind-nanolama-nextjs-module
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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/.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:

```tsx
// 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(path: string, init: RequestInit = {}): Promise {
  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("/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 ;
  if (error) return ;      // ApiError carries .status
  return ;
}
```

## 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`

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Deadlymind](https://github.com/Deadlymind)
- **Source:** [Deadlymind/nanolama](https://github.com/Deadlymind/nanolama)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-deadlymind-nanolama-nextjs-module
- Seller: https://agentstack.voostack.com/s/deadlymind
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
