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";
export default function MyComponent() {
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";
const session = await getServerSession(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 request
const response = await api.get("/users");
// Example POST request
const response = await api.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):
export interface User {
id: string;
name: string;
email: string;
}2. Write your API function (api/users.api.ts):
import { api } from "@/lib/api";
import { User } from "../types";
export async function getUsers(): Promise<User[]> {
const res = await api.get("/users");
return res.data;
}3. Create a TanStack Query hook (hooks/useUsers.ts):
import { useQuery } from "@tanstack/react-query";
import { getUsers } from "../api/users.api";
export function useUsers() {
return useQuery({
queryKey: ["users"],
queryFn: getUsers,
});
}4. Consume in a component (components/UserList.tsx):
"use client";
import { useUsers } from "../hooks/useUsers";
export default function UserList() {
const { data, isLoading, error } = useUsers();
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading users.</p>;
return (
<ul>
{data?.map((user) => (
<li key={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.ts
import { create } from "zustand";
interface ExampleState {
count: number;
increment: () => void;
}
export const useExampleStore = 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:
export const config = {
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.