Skip to content

Repository files navigation

Next.js Frontend Template

A production-ready, opinionated Next.js frontend template by Hybrid Interactive. Mirrors the structure of the FastAPI template on the frontend.

Conventions (read first): CLAUDE.md — the guardrail, plus the anti-patterns list
The rules, one per file: docs/rules/
How it fits together: docs/FRONTEND_ARCHITECTURE_GUIDE_V3.md
All documentation: docs/README.md

Working with an AI assistant? Point it at CLAUDE.md — Claude Code reads it automatically, and any other tool can be told to read it from the repo.


Tech Stack

Concern Library Version
Framework Next.js (App Router) 16.x
UI Library React 19.x
Component Primitives shadcn/ui latest
Server State TanStack Query 5.x
Client State Zustand 5.x
Styling Tailwind CSS (v4, CSS-native) 4.x
Forms React Hook Form + Zod RHF 7 + Zod 3
Icons Lucide React latest
Toasts Sonner 2.x
Dark Mode next-themes 0.4.x
Animations Framer Motion 12.x
Decimal math big.js 7.x
Auth BFF pattern (httpOnly cookies)
RBAC Built-in permissions system
Unit tests Vitest + Testing Library 4.x
Browser tests Playwright 1.x

Quick Start

1. Create your repo from the template

Click "Use this template" on GitHub → "Create a new repository" → name your repo → click "Create repository".

First time? Go to the template repo and click the green "Use this template" button.

2. Clone and enter the repo

git clone https://github.com/<your-org>/<your-repo>.git
cd <your-repo>

3. Install dependencies

npm install

4. Run the init command

node ncube.js init          # or: node ncube.js init my-app-name

This does three things in one step:

  • Sets the project name in package.json
  • Creates .env from .env.example (with your app name pre-filled)
  • Installs all shadcn/ui components into src/components/ui/

5. Configure your backend URL

# Edit .env — at minimum set:
NEXT_PUBLIC_API_URL="http://localhost:8000"

6. Start the development server

npm run dev

Open http://localhost:3000.


Legacy: bootstrap from a local clone

If you prefer to work from a local copy of the template instead of GitHub's "Use this template":

# From inside the nextjs-template directory:
node ncube.js create my-app [--variant base|rbac|full]
cd ../my-app
npm install
npm run dev

create is deprecated in favour of the template flow above. It still works but will show a notice.


Project Structure

src/
├── app/
│   ├── (auth)/                    # Unauthenticated pages
│   │   └── login/page.tsx
│   ├── (dashboard)/               # Authenticated pages — permission-gated
│   │   ├── config.ts              # PermissionedNavItem[], ROUTES — pure data, no JSX
│   │   ├── layout.tsx             # Seeds auth + role stores, renders the shell
│   │   └── dashboard/
│   │       ├── layout.tsx         # export const dynamic = "force-dynamic"
│   │       ├── error.tsx          # A page can crash without taking the shell
│   │       ├── loading.tsx        # Route skeleton
│   │       └── <feature>/page.tsx # Server component
│   ├── api/auth/                  # BFF route handlers — the only code holding cookies
│   ├── error.tsx  global-error.tsx  not-found.tsx
│   ├── globals.css                # Design tokens — the single source of truth
│   └── layout.tsx                 # Root layout + provider stack
│
├── components/
│   ├── data-view/                 # The list system: toolbar, table, paging, bulk actions
│   ├── ui/                        # shadcn primitives + Modal, StatusBadge, PageHeader
│   ├── shared/                    # DataTable, ReferencePicker, Field/DetailRow, lazy
│   ├── layout/                    # DashboardShell, PageLayout
│   ├── loading/                   # Global blocking overlay
│   ├── auth/                      # SessionExpiredDialog
│   └── providers/                 # QueryProvider
│
├── hooks/                         # useMediaQuery, useDebounce, useOlderPages
│
├── lib/
│   ├── api-client.ts              # The one HTTP client — stateless, same-origin
│   ├── auth/                      # BFF auth + session-expiry store
│   ├── permissions/               # RBAC: strict Permission union, hooks, mapping
│   ├── loading/                   # useBlockingMutation + the overlay store
│   ├── numeric/                   # Money & quantities on big.js — decimal strings
│   ├── date-utils.ts  timezone.ts # Instants vs business dates
│   ├── forms/                     # isFormDirty, useResetOnOpen
│   ├── reference/                 # Ungated dropdown feeds
│   ├── utilities/                 # Downloads, logger
│   ├── hooks/                     # useTabState, useZustandTabSync
│   └── <domain>/                  # types → transformers → api → hooks → index
│
├── proxy.ts                       # Route protection + the API rewrite to the backend
└── types/index.ts                 # AppError, NavItem, shared shapes

docs/
├── README.md                      # Documentation index
├── rules/                         # One rule per file — the detail behind CLAUDE.md
├── OPTIONAL_PARTS.md              # What you can delete, and how
└── FRONTEND_ARCHITECTURE_GUIDE_V3.md

Trim what you don't need

The template ships more than most projects use. Dead code is worse than absent code — it gets read, maintained and copied into new modules. Take it out early, while it is easy.

node ncube.js remove --list

Six subsystems can go cleanly: permissions/RBAC, decimal money, reference pickers, the blocking overlay, DataView, and dark mode. The command deletes the files, strips the imports and barrel exports, drops the dependencies, removes the matching rule doc, and then runs tsc --noEmit and tells you honestly whether it worked.

node ncube.js remove permissions --dry-run   # see what it would touch
node ncube.js remove permissions             # do it

Commit first — that is what makes it revertible. Details: docs/OPTIONAL_PARTS.md.


Checks

Four commands. All four must pass before a change is done — CI runs the same set.

npm run type-check   # tsc --noEmit
npm run lint         # eslint . — currently clean; keep it that way
npm test             # Vitest: pure modules + component logic, ~1s
npm run build        # catches what type-check alone cannot

And the browser suite, before a PR:

npm run test:e2e     # Playwright: the shared systems, desktop and mobile

Two layers on purpose. If an assertion depends on a real layout, a real animation or a real navigation, jsdom cannot see it — those live in e2e/. It starts its own mock backend, so it runs with nothing else running.


Variants

When you use "Use this template" on GitHub, you always get the full variant — everything included. No selection needed.

Variant Includes How to get it
full (default via template) Auth + full RBAC + access-control admin panel Use GitHub "Use this template"
rbac Auth + full RBAC, no admin panel Use template → delete src/app/(dashboard)/access-control/
base Auth + layout shell, no RBAC Use template → delete src/lib/permissions/ and src/app/(dashboard)/access-control/

For local bootstrapping (deprecated), the create command still supports --variant base|rbac|full.


NCube CLI — Domain Scaffolding

The ncube.js CLI mirrors the FastAPI fcube.py module generator. It scaffolds complete feature domains following the architecture conventions.

# Post-clone setup (name, .env, shadcn) — run once after cloning
node ncube.js init [my-app-name]

# Scaffold a new domain
node ncube.js startdomain Product
node ncube.js startdomain LeadManagement
node ncube.js startdomain InvoiceItem

# List existing domains
node ncube.js listdomains

# Install shadcn/ui components (included in init, but can run standalone)
node ncube.js setup

# (Deprecated) Bootstrap locally from the template directory
node ncube.js create my-app [--variant base|rbac|full]

What startdomain generates

Running node ncube.js startdomain Product creates:

src/lib/product/
├── types.ts          # Backend* + Frontend types + Zod schemas + status constants
├── transformers.ts   # snake_case ↔ camelCase conversions
├── api.ts            # Service functions via apiClient
├── hooks.ts          # React Query hooks + query key factory
├── store.ts          # Zustand UI state (modals, filters)
└── index.ts          # Barrel exports

src/components/product/
├── product-list.tsx  # DataTable with search + create button
├── product-form.tsx  # Dialog with create/edit/view modes
└── index.ts          # Barrel exports

src/app/(dashboard)/dashboard/product/
└── page.tsx          # Feature page → /dashboard/product

After running, follow the printed checklist to:

  1. Add your backend field types to types.ts
  2. Map fields in transformers.ts
  3. Add form fields in product-form.tsx
  4. Register the route in (dashboard)/config.ts

Adding a New Feature (Manual Checklist)

□ 1. node ncube.js startdomain <Name>
□ 2. Add backend + frontend types to src/lib/<name>/types.ts
□ 3. Complete transformers in src/lib/<name>/transformers.ts  (asEnum on every enum)
□ 4. Add form fields in src/components/<name>/<name>-form.tsx  (RHF + zod)
□ 5. Pass isDirty on the detail modal, and clear it on open
□ 6. Add PermissionedNavItem + ROUTES to src/app/(dashboard)/config.ts
□ 7. Add permission keys to src/lib/permissions/types.ts and helpers.ts
□ 8. If anything else picks this module by id, add it to REFERENCE_RESOURCES
□ 9. npm run type-check && npm run lint && npm test && npm run build

The full version, with the reasoning behind each step, is the New-feature checklist at the bottom of CLAUDE.md.


Authentication

Authentication uses the BFF (Backend-for-Frontend) pattern:

  • Tokens are stored in httpOnly cookies — JavaScript never reads them
  • The /api/auth/* route handlers proxy auth to the backend and set cookies
  • src/proxy.ts injects Authorization: Bearer <token> for /api/v1/* routes
  • On 401: apiClient auto-refreshes and retries once, then redirects to /login
Route Method Description
/api/auth/login POST Proxy login, set httpOnly cookies (access: 2hr, refresh: 7d)
/api/auth/me GET Return current user, silently refresh if expired
/api/auth/refresh POST Rotate tokens, set new cookies
/api/auth/logout POST Clear cookies

Permission System

Built-in RBAC with 4 default roles. Customize in src/lib/permissions/config.ts.

// Permission format: "resource.action"
const canManage = usePermission("content.manage");
const canViewOrManage = useAnyPermission(["content.view", "content.manage"]);

// Gate a nav item
{ name: "Settings", href: "/dashboard/settings", permission: "settings.view" }

Default roles: super_adminadminmemberviewer

super_admin always passes all permission checks.


Design Tokens

All design tokens live in src/app/globals.css — the single source of truth for colors, radius, and spacing. Tailwind v4 uses CSS-native configuration (no tailwind.config.js).

/* Customize in globals.css */
:root {
  --primary: oklch(0.205 0 0);  /* your brand color */
  --radius: 0.625rem;
}

Use semantic tokens in components — never hardcode hex colors:

className="bg-background text-foreground border-border"
className="text-primary bg-muted text-muted-foreground"

Blocking Loading System

All mutations use useBlockingMutation instead of raw useMutation. This automatically shows a global loading overlay during async operations.

// In hooks.ts — use this instead of useMutation
export function useCreateProduct() {
  return useBlockingMutation(
    { mutationFn: productApi.createProduct, onSuccess: () => toast.success("Created") },
    { source: "mutation", label: "Creating product…" },
  );
}

Environment Variables

Variable Required Description
NEXT_PUBLIC_APP_NAME No App display name
NEXT_PUBLIC_APP_URL No App URL (default: http://localhost:3000)
NEXT_PUBLIC_API_URL Yes Backend API base URL (e.g., http://localhost:8000)

Customizing Design Tokens

To brand the template for a specific project, update the CSS variables in src/app/globals.css:

:root {
  --primary: oklch(0.6 0.2 250);   /* your brand primary */
  --radius: 0.75rem;               /* border radius */
}
.dark {
  --primary: oklch(0.7 0.2 250);
}

Use oklch.com to find your brand colors in the oklch color space.


Engineering Conventions

Three documents, three jobs:

Document What it is for
CLAUDE.md The guardrail. Every convention in one page, plus a numbered anti-patterns list. Claude Code reads it automatically.
docs/rules/ The rules, one per file. What each rule is, how it works here, what was deliberately left undone, and the commands that prove the file still matches the code.
docs/FRONTEND_ARCHITECTURE_GUIDE_V3.md The narrative. How the pieces fit together, and the life of a request end to end.

Each rule has exactly one home, in docs/rules/. Everything else states it briefly and links there — so when a rule changes there is one file to edit.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages