A modern, production-ready Next.js template with a comprehensive development setup featuring custom component generation, type-safe CSS modules, robust error handling, infinite scroll with adapter pattern, accessible UI components, and Storybook integration.
- Tech Stack
- Key Features
- Prerequisites
- Getting Started
- Project Structure
- Component Generator
- Code Conventions
- Custom Hooks
- Data Adapters
- Shared UI Components
- Accessibility Features
- Storybook
- Testing
- Commit Conventions
- Styling System
- Configuration
- Deployment
- Contributing
- Framework: Next.js 15 with App Router
- Language: TypeScript
- Styling: CSS Modules with PostCSS
- State Management: TanStack Query (React Query)
- Testing: Vitest with React Testing Library
- Component Development: Storybook
- Linting/Formatting: Biome
- Git Hooks: Husky with Commitlint
- Package Manager: pnpm
- Node Version: 22.20.0 (managed via Volta)
- π¨ Component Library - Pre-built, accessible UI components (Input, Select, Checkbox, Toast)
- βΎοΈ Infinite Scroll - API-agnostic data fetching with adapter pattern
- πͺ Custom Hooks - useDebounce, useInfiniteScroll, useKeyboardNavigation, and more
- βΏ Accessibility - Screen reader support, keyboard navigation, ARIA compliance
- π Storybook - Interactive component development and documentation
- π‘οΈ Type Safety - Full TypeScript support with strict mode
- π Error Handling - Smart error boundaries with retry logic
- π¨ Styling System - CSS Modules with type-safe selectors and CSS variables
- β‘ Component Generator - CLI tool for rapid component scaffolding
- π§ͺ Testing - Vitest + React Testing Library with coverage reports
- π Code Quality - Biome for linting/formatting, Husky for git hooks
- Node.js 22.20.0 (or use Volta for automatic version management)
- pnpm 8.x or higher
# Install dependencies
pnpm install
# Start development server with Turbopack
pnpm devOpen http://localhost:3000 to view the application.
# Development
pnpm dev # Start dev server with Turbopack
pnpm build # Build for production
pnpm start # Start production server
# Testing
pnpm test # Run tests in watch mode
pnpm test:ui # Run tests with UI
pnpm test:coverage # Generate coverage report
# Storybook
pnpm storybook # Start Storybook dev server
pnpm build-storybook # Build Storybook for production
# Code Quality
pnpm format # Check formatting
pnpm format:write # Fix formatting
pnpm check # Lint and check code
pnpm check:write # Lint and auto-fix
# Component Generation
pnpm generate # Interactive component generatorβββ src/
β βββ app/ # Next.js App Router pages
β β βββ layout.tsx # Root layout
β β βββ page.tsx # Home page
β βββ components/ # Reusable components
β β βββ Accessibility/ # Accessibility components
β β β βββ Announcer/ # Screen reader announcers
β β β βββ Loading/ # Loading state announcer
β β β βββ Search/ # Search result announcer
β β βββ shared/ # Shared UI components
β β β βββ Input/ # Input component with variants
β β β βββ Select/ # Select dropdown component
β β β βββ Checkbox/ # Checkbox component
β β β βββ Toast/ # Toast notification system
β β βββ SmartErrorBoundary/ # Error boundary with retry logic
β βββ hooks/ # Custom React hooks
β β βββ infinite-scroll/ # Infinite scroll hooks
β β β βββ use-infinite-scroll.ts # Core infinite scroll logic
β β β βββ use-infinite-products.ts # Product-specific infinite scroll
β β β βββ use-prefetch.ts # Data prefetching
β β βββ keyboard-navigation/ # Keyboard navigation hooks
β β β βββ use-keyboard-navigation.ts # Core keyboard navigation
β β β βββ use-grid-navigation.ts # Grid navigation logic
β β βββ use-debounce.ts # Debounce hook
β β βββ use-filter-params.ts # URL filter params management
β β βββ use-infinite-data.ts # API-agnostic infinite data fetching
β βββ adapters/ # Data adapter pattern
β β βββ data-adapter.ts # Base adapter interface
β β βββ dummyjson-product-adapter.ts # DummyJSON API adapter
β β βββ custom-backend-product-adapter.ts # Custom backend adapter
β β βββ README.md # Adapter documentation
β βββ lib/ # Utility functions
β β βββ class-selectors.ts # Type-safe CSS module helpers
β β βββ api-client.ts # Fetch wrapper with error handling
β β βββ api-client.example.ts # API client usage examples
β βββ providers/ # React context providers
β β βββ QueryProvider.tsx # TanStack Query setup
β β βββ AnnouncementProvider.tsx # Screen reader announcements
β βββ styles/ # Global styles
β βββ globals.css # CSS variables & reset
β βββ utilities.module.css # Utility classes
βββ scripts/
β βββ generate-component/ # Component generator
β βββ index.ts # Generator CLI
β βββ templates/ # Component templates
βββ .storybook/ # Storybook configuration
β βββ main.ts # Main Storybook config
β βββ preview.tsx # Global preview settings
β βββ manager.ts # Manager config
β βββ theme.ts # Custom theme
βββ public/ # Static assets
Generate new components quickly with the custom CLI:
# Basic server component
pnpm generate MyComponent
# Client component with styles
pnpm generate MyButton --client --styles
# Component with props
pnpm generate UserCard --props "name: string; age: number; email: string"
# Custom directory (relative to /src)
pnpm generate Header --directory "components/layout"
# Force overwrite existing component
pnpm generate MyComponent --force-c, --client- Generate client component (default: server component)-s, --styles- Generate CSS module file-p, --props <props>- Define component props (format:"name: type; name2: type2")-d, --directory <path>- Target directory relative to/src(default:components)-f, --force- Overwrite existing component
Each component includes:
component-name.tsx- Component filecomponent-name.module.css- CSS module (if--stylesflag used)index.ts- Barrel export for cleaner imports
Type-safe CSS module usage with custom selectors:
import { createStrictClassSelector } from "@/lib/class-selectors";
import styles from "./component.module.css";
const css = createStrictClassSelector(styles);
function Component() {
return <div className={css("container")}>Content</div>;
}Reusable utility classes are available in utilities.module.css:
import utilities from "@/styles/utilities.module.css";
const cssUtils = createStrictClassSelector(utilities);
<div className={cssUtils("flex", "itemsCenter")} />;Use SmartErrorBoundary for robust error handling:
import SmartErrorBoundary from "@/components/SmartErrorBoundary";
<SmartErrorBoundary context="UserProfile" level="component" maxRetries={3} enableNavigation={false}>
<UserProfile />
</SmartErrorBoundary>;Levels:
component- Local component errorspage- Page-level errors (adds navigation buttons)app- Application-level errors (adds reload/home buttons)
API-agnostic infinite data fetching with adapter pattern:
import { useInfiniteData } from "@/hooks/use-infinite-data";
import { DummyJSONProductAdapter } from "@/adapters/dummyjson-product-adapter";
import { fetcher } from "@/lib/api-client";
const adapter = new DummyJSONProductAdapter(20);
function ProductList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteData({
queryKey: ["products"],
adapter,
fetcher,
filters: { search: "phone", category: "smartphones" },
});
return (
<div>
{data?.items.map((product) => (
<div key={product.id}>{product.title}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
Load More
</button>
)}
</div>
);
}Automatic infinite scrolling with IntersectionObserver:
import { useInfiniteScroll } from "@/hooks/infinite-scroll/use-infinite-scroll";
function InfiniteList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteData({
/* ... */
});
const { triggerRef } = useInfiniteScroll({
hasNextPage,
isFetchingNextPage,
onLoadMore: fetchNextPage,
rootMargin: "200px", // Load when 200px from bottom
threshold: 0.1,
});
return (
<div>
{data?.items.map((item) => (
<div key={item.id}>{item.title}</div>
))}
{/* Trigger element for intersection observer */}
<div ref={triggerRef} />
{isFetchingNextPage && <div>Loading more...</div>}
</div>
);
}Debounce rapidly changing values:
import { useDebounce } from "@/hooks/use-debounce";
function SearchInput() {
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 500); // 500ms delay
useEffect(() => {
// Only runs after user stops typing for 500ms
fetchResults(debouncedSearch);
}, [debouncedSearch]);
return <input value={search} onChange={(e) => setSearch(e.target.value)} />;
}Handle keyboard interactions with focus management:
import { useKeyboardNavigation } from "@/hooks/keyboard-navigation/use-keyboard-navigation";
function Modal({ onClose }) {
const { containerRef } = useKeyboardNavigation({
onEscape: onClose,
onEnter: handleSubmit,
trapFocus: true, // Keep focus within modal
restoreFocus: true, // Restore focus when unmounted
});
return <div ref={containerRef}>{/* Modal content */}</div>;
}Navigate items in a grid with arrow keys:
import { useGridNavigation } from "@/hooks/keyboard-navigation/use-grid-navigation";
function ProductGrid({ products }) {
const { containerRef, focusedIndex } = useGridNavigation({
itemCount: products.length,
columns: 4,
onSelect: (index) => openProduct(products[index]),
});
return (
<div ref={containerRef}>
{products.map((product, index) => (
<div key={product.id} data-focused={focusedIndex === index}>
{product.title}
</div>
))}
</div>
);
}The adapter pattern allows you to swap APIs without changing your application code. See the comprehensive Adapter Documentation for detailed examples.
// 1. Create an adapter for your API
import type { DataAdapter } from "@/adapters/data-adapter";
class MyAPIAdapter implements DataAdapter<Product, Filters, Response, number> {
initialPageParam = 1;
buildURL(filters: Filters, page: number): string {
return `/api/products?page=${page}&search=${filters.search}`;
}
parseResponse(response: Response) {
return {
items: response.data,
total: response.total,
hasNextPage: response.hasMore,
};
}
getNextPageParam(lastPage, allPages) {
return lastPage.hasNextPage ? allPages.length + 1 : undefined;
}
}
// 2. Use with useInfiniteData
const adapter = new MyAPIAdapter();
const { data } = useInfiniteData({ adapter, fetcher, filters });Benefits:
- π Swap APIs by changing adapter only
- π§ͺ Easy testing with mock adapters
- π¦ Reusable across different data sources
- π‘οΈ Full TypeScript support
Included Adapters:
DummyJSONProductAdapter- Skip-based pagination exampleCustomBackendProductAdapter- Page-based pagination example
Text input with variants and states:
import { Input } from "@/components/shared";
<Input
variant="outlined" // outlined | filled | ghost
size="medium" // small | medium | large
error="Invalid email"
placeholder="Enter email..."
/>;Dropdown select component:
import { Select } from "@/components/shared";
<Select
options={[
{ value: "1", label: "Option 1" },
{ value: "2", label: "Option 2" },
]}
onChange={(value) => console.log(value)}
placeholder="Select an option"
/>;Accessible checkbox component:
import { Checkbox } from "@/components/shared";
<Checkbox label="Accept terms" checked={accepted} onChange={setAccepted} />;Display toast notifications with the toast system:
import { toast } from "@/components/shared/Toast";
// Add ToastProvider to your layout
import { ToastProvider } from "@/components/shared/Toast";
function RootLayout({ children }) {
return (
<ToastProvider maxToasts={5} position="top-right">
{children}
</ToastProvider>
);
}
// Use toast in components
toast.success("Operation completed!");
toast.error("Something went wrong");
toast.info("New message received");
toast.warning("Please save your work");
// With custom duration and actions
toast.success("File uploaded", {
duration: 5000,
action: { label: "View", onClick: () => navigate("/files") },
});Screen reader announcements for dynamic content:
import { AnnouncementProvider, useAnnouncement } from "@/providers/AnnouncementProvider";
// Add to root layout
function RootLayout({ children }) {
return <AnnouncementProvider>{children}</AnnouncementProvider>;
}
// Use in components
function MyComponent() {
const { announce } = useAnnouncement();
const handleAction = () => {
// Announce to screen readers
announce("Item added to cart", "polite");
};
}- LoadingAnnouncer - Announces loading states
- SearchAnnouncer - Announces search results count
import { LoadingAnnouncer } from "@/components/Accessibility/Announcer/Loading";
import { SearchAnnouncer } from "@/components/Accessibility/Announcer/Search";
<LoadingAnnouncer isLoading={isLoading} message="Loading products" />
<SearchAnnouncer resultCount={results.length} query={searchQuery} />Develop and document components in isolation:
# Start Storybook
pnpm storybook
# Build static Storybook
pnpm build-storybookView component stories at http://localhost:6006
Features:
- Interactive component development
- Automatic documentation
- Accessibility testing with a11y addon
- Responsive viewport testing
- Component interaction testing
All shared components include Storybook stories (.stories.tsx files).
Tests are powered by Vitest and React Testing Library:
# Run tests
pnpm test
# Watch mode with UI
pnpm test:ui
# Generate coverage
pnpm test:coverageExample test:
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import Component from "./component";
describe("Component", () => {
it("renders correctly", () => {
render(<Component />);
expect(screen.getByText("Hello")).toBeInTheDocument();
});
});This project uses Conventional Commits enforced by Commitlint.
<type>(<scope>): <description>
[optional body]
[optional footer]
feat- New featurefix- Bug fixdocs- Documentation changesstyle- Code style changes (formatting, etc.)refactor- Code refactoringperf- Performance improvementstest- Adding or updating testschore- Maintenance tasksci- CI/CD changesbuild- Build system changesrevert- Revert previous commit
Scopes must be UPPERCASE:
CORE- Core functionalityAPI- API relatedUI- User interfaceDB- DatabaseCONFIG- ConfigurationAUTH- AuthenticationSEARCH- Search functionalityCHECKOUT- Checkout processCI- CI/CDBUILD- Build process- Branch name (automatically detected)
git commit -m "feat(UI): add user profile component"
git commit -m "fix(API): resolve authentication timeout"
git commit -m "docs(CORE): update README with new features"All design tokens are defined in globals.css:
/* Colors */
--color-primary
--color-secondary
--color-text-primary
--color-text-secondary
--color-action-primary
/* Spacing */
--spacing-xs to --spacing-2xl
/* Border Radius */
--radius-sm to --radius-xl
/* Font Sizes */
--font-size-xs to --font-size-4xl
/* Font Weights */
--font-weight-normal to --font-weight-boldDark mode is automatically supported via prefers-color-scheme:
@media (prefers-color-scheme: dark) {
:root {
--color-main: hsl(0, 0%, 10%);
/* ... */
}
}The project includes VS Code settings for:
- Auto-format on save with Biome
- Organize imports automatically
- CSS validation
- TypeScript integration
Configured with:
postcss-custom-media- Custom media queriesautoprefixer- Vendor prefixes
Format and lint settings in biome.json:
- 120 character line width
- Tabs for indentation (4 spaces)
- Double quotes
- ES5 trailing commas
- TanStack Query Docs
- Storybook Documentation
- Biome Documentation
- Vitest Documentation
- Adapter Pattern Guide - Local documentation
The easiest way to deploy is using Vercel:
- Push your code to a Git repository
- Import your project to Vercel
- Vercel will auto-detect Next.js and configure deployment
- Your app will be live!
See Next.js deployment documentation for other hosting options.
- Follow the commit conventions
- Run
pnpm check:writebefore committing (automated via Husky) - Write tests for new features
- Update documentation as needed
This project is open source and available under the MIT License.