This project is a frontend challenge built with Next.js App Router, focusing on modern React patterns such as Server Components, Route Handlers, runtime validation, SEO, and progressive client interactivity.
The goal is not only to make the application work, but to demonstrate clean architecture, explicit trade-offs, and production-ready decisions.
- Node.js 22.13.1
- Next.js 16 (App Router)
- React 19
- TypeScript
- Mantine UI
- Zod (runtime schema validation)
- Zustand (lightweight client-side state management)
- Jest + Testing Library (unit tests)
- Playwright (end-to-end tests)
- Husky + Commitlint (commit discipline)
- Vercel (deployment)
src/
├── app/
│ ├── books/ # Server-rendered pages
│ ├── favorites/ # Server page + client island
| ├── search/ # Client-driven search & filtering UI
│ ├── api/ # Route Handlers (mock API)
│ ├── sitemap.ts
│ ├── robots.ts
│ └── middleware.ts
│
├── components/
│ ├── book/ # BookCard, BookGrid
│ ├── favorites/ # Client-only interactive components
│ ├── search/
│ ├── ui/
│ └── common/
│
├── lib/
│ ├── books/ # Repository layer + Zod schemas
│ ├── seo/ # SEO helpers (canonical URLs, JSON-LD builders, metadata utilities)
│ └── data.ts # Mock dataset (single source of truth)
│
├── store/
│ └── favoritesStore.ts # Zustand client store
│
├── styles/
│ └── theme.ts # Design tokens and global styling setup (Mantine theme, CSS variables)
│
└── tests/
├── unit/
└── e2e/
Server Components are the default. Client Components are used only when browser-only APIs or user interaction are required.
Server Components are responsible for:
- Data fetching
- SEO metadata generation
- Static rendering and revalidation
- Error and not-found boundaries
Client Components are responsible for:
- User interaction (search, debounce)
- Client-side state (favorites)
- Event handlers and transitions
This follows the App Router mental model:
Server = data & structure
Client = interaction & state
The application uses a local in-memory dataset (BOOKS) as the single source of truth.
BOOKS → repository → Server Components
BOOKS → Route Handlers (/api/books)
Why not fetch /api/books internally?
Self-fetching during build or prerender (especially on Vercel) is fragile and often leads to runtime failures.
Chosen trade-off:
- Server Components read directly from the repository
- Route Handlers expose the same data as a mock API
This avoids build-time issues while keeping the API contract intact.
All data exposed by the repository and API routes is validated using Zod.
Benefits:
- Guarantees runtime correctness
- Acts as a contract between layers
- Prevents silent data shape mismatches
Because the dataset is static and local:
- Page-level revalidation is used (
export const revalidate = 60) cache()is used inside the repository to deduplicate calls within the same render cycle
This satisfies the caching and revalidation requirement without relying on internal HTTP fetches.
Favorites are implemented as a pure client-side concern using Zustand with persistence.
Key points:
- Stored in
localStorage - No server dependency
- Minimal and intentional state management
Architecture:
- Server pages fetch the full book list
- Client components filter books using favorite IDs stored in Zustand
This cleanly separates server data from user preference state.
A lightweight middleware.ts demonstrates a cross-cutting concern:
- Reads the
Accept-Languageheader - Derives a default locale (
tr/en) - Applies it consistently before rendering
This showcases Edge Runtime usage without unnecessary complexity.
SEO is handled entirely on the server using the Next.js Metadata API:
- Dynamic titles and descriptions
- Open Graph and Twitter Card metadata
- Canonical URLs
- Structured data (JSON-LD) on detail pages
sitemap.tsandrobots.ts
This ensures correctness, performance, and crawler compatibility.
- Mantine is used as the UI library
- Custom styles use CSS Modules
- BEM naming is applied where appropriate
- Focus on consistent spacing, typography, and empty states
- Jest + React Testing Library
- Covers core UI components and states
- Tests behavior rather than implementation details
Run:
yarn test- Playwright
- Covers a critical user flow (list → detail)
Run:
yarn e2eThis project enforces Conventional Commits using Husky and Commitlint.
Required format:
type(scope): message
Examples:
feat(favorites): add favorites feature
fix(search): handle empty query
refactor(ui): extract favorite button
Invalid commit messages are blocked locally before commit.
yarn install
yarn devThe project is deployed on Vercel.
- Production deployments are triggered from the
mainbranch - CI ensures code quality before merge
- No manual deployment steps are required
This project intentionally avoids over-engineering and focuses on:
- Clear architectural boundaries
- Explicit and documented trade-offs
- App Router best practices
- Maintainability over clever abstractions
The resulting structure mirrors how a production-grade Next.js App Router application can be built using mock data while remaining robust, testable, and deploy-safe.