Overview
nextjs-ecc-starter is a batteries-included boilerplate for the Next.js 16 App Router. Every new Next project rebuilds the same set of choices: UI lib, form lib, env validator, state store, CI, security baseline. This repo makes those choices — opinionated, modern, audited — so day one is product code instead of scaffolding.
It's the SSR sibling of react-ecc-starter: same ECC (Everything Claude Code) philosophy, different platform — that one is Vite SPA, this one is Next.js App Router with Server Components by default, Server Actions, and ISR. The repo is registered as a GitHub template (is_template: true), so the official quickstart is "Use this template" rather than git clone.
Released 2026-05-16 — early but stable: from v0.1 it ships AgentShield's security baseline, Dependabot security updates, and a real Vitest 80% coverage gate (not just configured — actually enforced).
- Build: Next.js 16.2 + React 19.2 + React Compiler (
babel-plugin-react-compiler 1.0) — no manualuseMemo/useCallback. - Routing: App Router, Server Components by default, route handlers
api/health+api/contact, and a Server Action atactions/contact.ts. - Styling: Tailwind CSS v4 (
@tailwindcss/postcss) + shadcn-style primitives (Radix avatar/dialog/dropdown/label/separator/slot + CVA + clsx + tailwind-merge + lucide + sonner) — primitives owned in-repo, no CLI. - Forms: React Hook Form 7 + Zod 3 +
@hookform/resolvers— with a Server Action sample that validates client + server against the same schema. - State: Zustand 5 (
persist+skipHydration) — sidesteps the classic SSR theme-mismatch flash. - Env:
@t3-oss/env-nextjsinsrc/env.ts— missing vars fail the build instead of crashing the first request. - Tests: Vitest 2 (jsdom + Testing Library, 80% threshold) plus Playwright 1.49 across Chromium + Firefox + WebKit.
- Security:
ecc-agentshield 1.4with a committed baseline, a hook that blocksNEXT_PUBLIC_*leaks, and amiddleware.tsthat sets security headers. - AI tooling:
.claude/(12 agents, 13 commands, 14 skills) +.cursor/(mirrored rules) +.mcp.json— Claude Code, Cursor, and MCP all usable out of the box.
Why I built it
Standing in front of an empty npx create-next-app, every new project re-litigates the same ten decisions: where do UI primitives come from, which form lib, runtime or compile-time validation, which env lib, which state store, how to test layouts, which browsers in CI, which security scanner, where the AI prompts live, conventional commits or not. That's 1-2 weeks of yak-shaving each time.
nextjs-ecc-starter commits to those decisions — strict enough for production, open enough to layer in your own domain logic without fighting the framework. The build-time env validation alone earns its keep: missing env vars fail the CI build, not the first production request with a 500.
The differentiator is the full Everything Claude Code integration: .claude/ with 12 agents + 13 commands + 14 skills, a mirrored .cursor/ rule set, and .mcp.json for MCP servers. Instead of configuring three agentic IDEs from scratch on every project, this repo ships with them — open Claude Code (or Cursor) right after clone, and the review/scaffold/debug patterns are already there.
One honest note: v0.3.0 was released on 2026-05-16 — the repo is fresh, star count is low. The foundation is audited (AgentShield baseline, 80% coverage gate, Dependabot), but the community is new. If you want a starter "a solo founder ships daily code on top of", this fits; if you need "thousands of users have battle-tested it", give it a few months.
Architecture
Organised around App Router conventions, with a clean server/client boundary.
src/app/— App Router: rootlayout.tsx, landing page,(examples)/showcase (components, forms, data-fetching ISR-vs-polling side-by-side),actions/contact.tsServer Action, route handlersapi/health+api/contact, plussitemap.ts/robots.ts/manifest.ts/loading.tsx/error.tsx/not-found.tsx.src/components/—ui/for in-repo shadcn-style primitives,layout/forThemeProvider+ThemeToggle+SiteHeader+SiteFooter+Container, pluserror-boundary.tsx.src/hooks/—useDebounce,useMediaQuery,useMounted,useLocalStorage(no third-party utility-hook lib).src/lib/—utils.ts(cn()),format.ts,fetcher.ts(a typed wrapper that returns Rust-styleResult<T>instead of throwing),logger.ts,validations/for shared Zod schemas.src/stores/—use-theme-store.tsZustand store withpersist+skipHydration(avoids the theme-flash hydration mismatch).src/env.ts—@t3-oss/env-nextjswith splitserver/clientschemas, validated at build time.middleware.ts(root) — sets security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy).tests/— Playwright E2E across Chromium + Firefox + WebKit, configured inplaywright.config.ts..claude/+.cursor/+.mcp.json+AGENTS.md— the AI tooling layer, audited by AgentShield.
Path alias @/ points at ./src/ — no ../../. ESLint flat config (eslint.config.mjs) + Prettier 3 + husky 9 + lint-staged 15 + commitlint 19 (conventional commits, enforced on both pre-commit and commit-msg).
Security pipeline: security:scan runs AgentShield plus a custom shell wrapper in scripts/security-scan.sh; security:baseline regenerates .agentshield-baseline.json (~21 KB approved-finding snapshot). A hook blocks NEXT_PUBLIC_* when a server-only secret would leak into the client bundle.
Installation
Get the project running locally in a couple of commands.
gh repo create my-app --template pldkhoi/nextjs-ecc-starter --public --clone
cd my-appgit clone https://github.com/pldkhoi/nextjs-ecc-starter.git my-app
cd my-appbun installcp .env.example .env.local
# set NEXT_PUBLIC_SITE_URL and NEXT_PUBLIC_APP_NAME
bun dev # http://localhost:3000bunx playwright installUsage
Common patterns and snippets to get started fast.
// app/(examples)/data-fetching/page.tsx
import { Container } from '@/components/layout/container';
export const revalidate = 60; // ISR — regenerate at most every 60s
async function fetchStatus() {
const res = await fetch('https://api.github.com/repos/pldkhoi/nextjs-ecc-starter', {
next: { revalidate: 60, tags: ['repo-stats'] },
});
if (!res.ok) return null;
return res.json() as Promise<{ stargazers_count: number; pushed_at: string }>;
}
export default async function DataFetchingPage() {
const repo = await fetchStatus();
return (
<Container>
<h1 className="text-2xl font-semibold">Repo snapshot</h1>
{repo ? (
<p>
{repo.stargazers_count} stars · last push {new Date(repo.pushed_at).toLocaleString()}
</p>
) : (
<p>GitHub API unreachable.</p>
)}
</Container>
);
}// src/lib/validations/contact.ts
import { z } from 'zod';
export const contactSchema = z.object({
name: z.string().min(2).max(80),
email: z.string().email(),
message: z.string().min(10).max(2000),
});
export type ContactInput = z.infer<typeof contactSchema>;
// app/actions/contact.ts
'use server';
import { contactSchema, type ContactInput } from '@/lib/validations/contact';
export async function sendContact(input: ContactInput) {
const parsed = contactSchema.safeParse(input); // re-validate server-side
if (!parsed.success) return { ok: false, error: parsed.error.flatten() };
// ... persist or forward to email provider
return { ok: true };
}
// app/(examples)/forms/contact-form.tsx
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { contactSchema, type ContactInput } from '@/lib/validations/contact';
import { sendContact } from '@/app/actions/contact';
export function ContactForm() {
const form = useForm<ContactInput>({ resolver: zodResolver(contactSchema) });
return (
<form onSubmit={form.handleSubmit(async (values) => {
const result = await sendContact(values);
if (!result.ok) form.setError('root', { message: 'Failed' });
})}>
<input {...form.register('email')} type="email" />
<textarea {...form.register('message')} />
<button type="submit">Send</button>
</form>
);
}// src/stores/use-theme-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type Theme = 'light' | 'dark' | 'system';
interface ThemeState {
theme: Theme;
setTheme: (theme: Theme) => void;
}
export const useThemeStore = create<ThemeState>()(
persist(
(set) => ({
theme: 'system',
setTheme: (theme) => set({ theme }),
}),
{
name: 'theme',
skipHydration: true, // hydrate manually after mount to avoid SSR mismatch
},
),
);
// src/components/layout/theme-provider.tsx
'use client';
import { useEffect } from 'react';
import { useThemeStore } from '@/stores/use-theme-store';
export function ThemeProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
useThemeStore.persist.rehydrate(); // manual rehydrate after mount
}, []);
return <>{children}</>;
}Roadmap
- Promote the auth recipes (NextAuth v5, Clerk, Lucia) from
docs/auth-setup.mdinto opt-in branches you can clone directly. - DB recipes (Prisma + Postgres, Drizzle + Neon, Supabase) split into separate branches — pick one, clone, no dead-code cleanup.
- Extract
@kpboards/next-uias a standalone npm package once bothnextjs-ecc-starterandreact-ecc-starterstabilise — primitives shared cross-project. - Derivative template
next-ecc-starter-shop: Stripe checkout via Server Actions, product catalog, cart on Zustand. - Next 16 → Next 17 migration guide once the RC stabilises, with a codemod to auto-fix breaking changes.
- ECC: add a
/release-notescommand that auto-drafts CHANGELOG entries from conventional commits.