Next.js 16 + React 19 + Tailwind v4 + shadcn (radix-maia). Validated env, typed proxy, and the standard set of route conventions wired up.
- Next.js 16 App Router (
proxy.ts,forbidden.tsx,unauthorized.tsx) - React 19, TypeScript (strict)
- Tailwind v4 with shadcn
radix-maiastyle @t3-oss/env-nextjs+ Zod 4 for build-time env validation
pnpm install
cp .env.example .env.local # fill in values
pnpm devOpen http://localhost:3000.
| Command | What it does |
|---|---|
pnpm dev |
Dev server |
pnpm build |
Production build (validates env) |
pnpm start |
Run the production build |
pnpm lint |
ESLint |
pnpm typecheck |
tsc --noEmit |
Schemas live in src/env/, split by side:
src/env/server.ts— server-only vars. t3-env throws at runtime if a client component reads it.src/env/client.ts—NEXT_PUBLIC_*vars, safe everywhere.
Both are imported in next.config.ts so the build fails on any malformed value. Set SKIP_ENV_VALIDATION=1 to bypass (Docker, lint-only CI).
| Var | Side | Required | Notes |
|---|---|---|---|
NODE_ENV |
server | auto | development / test / production |
API_BASE_URL |
server | optional | Upstream API for server-side fetch |
API_SECRET |
server | optional | Bearer token forwarded server-side |
NEXT_PUBLIC_APP_URL |
client | optional | Defaults to http://localhost:3000 |
NEXT_PUBLIC_APP_NAME |
client | optional | Defaults to Next Starter |
Use it like:
// Server code (route handlers, Server Components, Server Actions)
import { env } from "@/env/server";
await fetch(`${env.API_BASE_URL}/users`, {
headers: { Authorization: `Bearer ${env.API_SECRET}` },
});
// Client code or shared metadata
import { env } from "@/env/client";
console.log(env.NEXT_PUBLIC_APP_URL);Replaces the legacy middleware.ts (Next.js 16 renamed it). It runs before the cache and:
- Generates an
x-request-idand forwards it to the request headers + response - Sets baseline security headers (
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy) - Skips static assets via the matcher
Add auth gating, rewrites, or redirects there as needed. Note: runtime config is not allowed in proxy.ts — it always runs on Node.js.
| File | Purpose |
|---|---|
src/app/loading.tsx |
Root suspense fallback |
src/app/error.tsx |
Client error boundary (unstable_retry) |
src/app/not-found.tsx |
404 page |
src/app/forbidden.tsx |
403 page (calls forbidden()) |
src/app/unauthorized.tsx |
401 page (calls unauthorized()) |
src/app/robots.ts |
/robots.txt |
src/app/sitemap.ts |
/sitemap.xml |
src/app/api/health/route.ts |
Liveness probe at GET /api/health |
forbidden.tsx and unauthorized.tsx require experimental.authInterrupts: true, already enabled in next.config.ts.
src/
├── app/ # App Router routes & file conventions
│ └── api/health/ # Liveness probe
├── components/ui/ # shadcn components (added via `pnpm dlx shadcn@latest add ...`)
├── lib/utils.ts # cn() helper
├── env/
│ ├── server.ts # Server-only env schema
│ └── client.ts # NEXT_PUBLIC_* env schema
└── proxy.ts # Next.js 16 proxy (formerly middleware)