A production-ready, opinionated Next.js 16 template with a clean feature-driven architecture, authentication, typed API layer, and developer tooling pre-configured out of the box.
- Tech Stack
- Project Structure
- Getting Started
- Environment Variables
- Authentication
- API Layer
- Feature Pattern
- State Management
- Routing & Middleware
- UI Components
- Developer Tooling
- Scripts Reference
- Commit Convention
| Category | Library / Tool | Version |
|---|---|---|
| Framework | Next.js | 16.1.4 |
| Language | TypeScript | ^5 |
| Styling | Tailwind CSS | ^4 |
| UI Components | Shadcn UI + Radix UI | Latest |
| Icons | Lucide React | ^0.553.0 |
| Authentication | NextAuth.js | ^4.24 |
| Data Fetching | Axios + TanStack Query | ^1.13 / ^5.90 |
| Forms | React Hook Form + Zod | ^7.66 / ^3.25 |
| State Management | Zustand | ^5.0 |
| Rich Text Editor | Tiptap | ^3.17 |
| Animations | Framer Motion | ^12 |
| Notifications | Sonner | ^2.0 |
| Testing | Jest + Testing Library | ^30 |
| Linting | ESLint + Prettier | ^9 / ^3 |
| Commit Hooks | Husky + Lint-staged + Commitlint | ^8 / ^16 / ^20 |
src/
├── app/ # Next.js App Router
│ ├── api/
│ │ └── auth/
│ │ └── [...nextauth]/ # NextAuth route handler
│ │ └── route.ts
│ ├── layout.tsx # Root layout with global providers
│ ├── page.tsx # Home page
│ ├── globals.css # Global styles
│ └── not-found.tsx # Custom 404 page
│
├── Providers/
│ ├── MainProviders.tsx # TanStack Query client provider
│ └── Provider.tsx # NextAuth SessionProvider
│
├── components/
│ ├── ui/ # Shadcn UI primitives (Button, Input, Dialog, etc.)
│ └── shared/ # Cross-feature shared components
│
├── features/
│ ├── auth/
│ │ └── api/
│ │ └── refresh-token.api.ts # Token refresh used by NextAuth
│ └── sample-feature/ # Reference architecture — copy this for new features
│ ├── api/ # API call functions (uses src/lib/api.ts)
│ ├── components/ # Feature-specific UI components
│ ├── hooks/ # TanStack Query custom hooks
│ └── types.ts # Feature TypeScript types
│
├── hooks/ # Global reusable hooks
│ └── readme.md
│
├── lib/
│ ├── api.ts # Axios instance with auth interceptors
│ ├── utils.ts # Utility functions (cn, etc.)
│ ├── indexed-db-storage.ts # IndexedDB helpers
│ └── readme.md
│
├── store/
│ ├── ui.store.ts # Global UI state (Zustand)
│ └── readme.md
│
├── types/
│ ├── next-auth.d.ts # Extended NextAuth TypeScript types
│ └── readme.md
│
├── tests/ # Jest unit test files
│
└── proxy.ts # Next.js middleware for RBAC routing
- Node.js 18+
- npm or pnpm
git clone <your-repo-url>cd<project-folder>npm installcp example.env.local .env.localEdit .env.local with your actual values (see Environment Variables).
npm run devOpen http://localhost:3000 in your browser.
Copy example.env.local to .env.local and fill in the following:
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_API_URL | ✅ | Base URL of your backend REST API (e.g. http://localhost:5001/api/v1) |
NEXTAUTH_SECRET | ✅ | A random secret string used to encrypt JWT sessions. Generate with: openssl rand -hex 32 |
NEXTAUTH_URL | ✅ | The canonical URL of your deployed app (e.g. http://localhost:3000 for local) |
NEXT_PUBLIC_SOCKET_URL | ⬜ | Optional WebSocket server URL |
Security: Never commit your
.env.localfile. It is already listed in.gitignore.
Authentication is handled via NextAuth.js v4 using its Credentials Provider strategy.
- The user submits their email and password.
- NextAuth calls your backend
POST /auth/loginendpoint. - On success, the user object and
accessTokenare stored in a JWT session. - The JWT is automatically refreshed when it expires (1 hour default), via
POST /auth/refresh-access-token. - If the refresh fails, the session is destroyed and the user is signed out.
The session is extended to include custom fields. The types are declared in src/types/next-auth.d.ts:
session.user={id: string;
name: string;
email: string;
image: string;// Maps to profileImage from backend
role: string;// e.g. "ADMIN" | "USER"};session.accessToken=string;session.refreshToken=string;Client-side (in a Client Component):
"use client";import{useSession}from"next-auth/react";exportdefaultfunctionMyComponent(){const{data: session}=useSession();// session.accessToken, session.user.role, etc.}Server-side (in a Server Component or API Route):
import{getServerSession}from"next-auth";import{authOptions}from"@/app/api/auth/[...nextauth]/route";constsession=awaitgetServerSession(authOptions);All HTTP requests go through the centralized Axios instance at src/lib/api.ts.
- Auto-auth injection: Every request automatically includes the
Bearertoken from the active NextAuth session. - Auto-retry on 401: If a request fails with a 401, it tries to use the refreshed token from the session and retries once.
- Auto-signout: If the token refresh has failed (
RefreshAccessTokenError), the user is immediately signed out and redirected to/login.
import{api}from"@/lib/api";// Example GET requestconstresponse=awaitapi.get("/users");// Example POST requestconstresponse=awaitapi.post("/users",{name: "John"});Note: You never need to manually set
Authorizationheaders. The interceptor handles it globally.
All business logic lives inside src/features/. Each feature is a self-contained module with a consistent internal structure. Use src/features/sample-feature as your starting point.
src/features/your-feature/
├── api/
│ └── your-feature.api.ts # Raw API call functions
├── components/
│ └── YourComponent.tsx # Feature UI components
├── hooks/
│ └── useYourFeature.ts # TanStack Query hooks
└── types.ts # TypeScript types for this feature
1. Define your types (types.ts):
exportinterfaceUser{id: string;name: string;email: string;}2. Write your API function (api/users.api.ts):
import{api}from"@/lib/api";import{User}from"../types";exportasyncfunctiongetUsers(): Promise<User[]>{constres=awaitapi.get("/users");returnres.data;}3. Create a TanStack Query hook (hooks/useUsers.ts):
import{useQuery}from"@tanstack/react-query";import{getUsers}from"../api/users.api";exportfunctionuseUsers(){returnuseQuery({queryKey: ["users"],queryFn: getUsers,});}4. Consume in a component (components/UserList.tsx):
"use client";import{useUsers}from"../hooks/useUsers";exportdefaultfunctionUserList(){const{ data, isLoading, error }=useUsers();if(isLoading)return<p>Loading...</p>;if(error)return<p>Error loading users.</p>;return(<ul>{data?.map((user)=>(<likey={user.id}>{user.name}</li>))}</ul>);}5. Add a route in src/app/(your-group)/your-route/page.tsx and import the component.
Global UI state is managed with Zustand in src/store/.
// src/store/example.store.tsimport{create}from"zustand";interfaceExampleState{count: number;increment: ()=>void;}exportconstuseExampleStore=create<ExampleState>((set)=>({count: 0,increment: ()=>set((state)=>({count: state.count+1})),}));Convention: Keep stores small and focused. One store per concern (e.g.
ui.store.ts,sidebar.store.ts).
The src/proxy.ts file is a Next.js Middleware that runs at the edge before any page renders. It handles Role-Based Access Control (RBAC).
| Condition | Action |
|---|---|
Unauthenticated user visits /dashboard | Redirect to /login?callbackUrl=... |
Non-admin user visits /dashboard | Redirect to / |
| All other requests | next() — passes through |
Edit the matcher in src/proxy.ts to control which paths trigger the middleware:
exportconstconfig={matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sitemap.xml|robots.txt).*)",],};Create a folder with parentheses in src/app/ to group routes without affecting the URL:
src/app/
├── (dashboard)/
│ └── dashboard/
│ └── page.tsx → URL: /dashboard
├── (auth)/
│ └── login/
│ └── page.tsx → URL: /login
All primitive UI components come from Shadcn UI and live in src/components/ui/.
| Component | Description |
|---|---|
Button | Multi-variant button (default, outline, ghost, etc.) |
Input | Styled form input |
Label | Accessible form label |
Dialog | Modal dialog |
Select | Dropdown select |
Checkbox | Accessible checkbox |
Avatar | User avatar with fallback |
Dropdown Menu | Context/dropdown menus |
Accordion | Collapsible accordion panels |
| And more... | Run npx shadcn add <component> to add more |
npx shadcn add <component-name># e.g.
npx shadcn add sheet
npx shadcn add calendarConfigured in eslint.config.mjs. Extends eslint-config-next.
npm run lintConfig in .prettierrc. Auto-formats on commit via lint-staged.
# Format all files manually
npx prettier --write .Pre-commit hooks auto-run linting on staged files before every commit.
Config in .lintstagedrc.json and .lintstagedrc.
Unit and integration tests live in src/tests/.
npm run test# Run all tests once
npm run test:watch # Run tests in watch modenpm run type-check # Runs tsc --noEmit| Script | Command | Description |
|---|---|---|
| Development | npm run dev | Start the dev server with Webpack |
| Build | npm run build | Create an optimized production build |
| Start | npm run start | Start the production server |
| Lint | npm run lint | Run ESLint |
| Test | npm run test | Run Jest tests |
| Test Watch | npm run test:watch | Run tests in watch mode |
| Type Check | npm run type-check | TypeScript type validation |
| Commit | npm run commit | Interactive commit with Commitizen |
This project enforces Conventional Commits via Commitlint. Every commit message must follow the format:
<type>: <subject>
| Type | When to Use |
|---|---|
feat | A new feature |
fix | A bug fix |
docs | Documentation changes only |
style | Code style / formatting (no logic change) |
refactor | Code refactoring (no feature or bug fix) |
test | Adding or updating tests |
build | Build system or dependency changes |
chore | Maintenance tasks |
ci | CI/CD configuration changes |
perf | Performance improvements |
revert | Reverting a previous commit |
security | Security-related changes |
feat: add user profile page
fix: resolve token expiry loop on 401
docs: update environment variable guide
refactor: extract api calls into feature modulenpm run commitThis launches an interactive CLI to guide you through writing a valid commit message.
This project is a starter template. You are free to use, modify, and distribute it.