Install
$ agentstack add skill-deadlymind-nanolama-nextjs-module ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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/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(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/jsonunconditionally. For aFormData
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
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 (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-csrfreact-queryrbac-permissionszod-formsdrf-zod-contracti18n-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
- Source: Deadlymind/nanolama
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.