Frontend Architecture Proposal

Al Muthana
Frontend Architecture

Complete Frontend Architecture & Package Reference • Next.js 15 App Router • April 2026
35+
Packages
~50KB
Bundle (gzip)
RTL
Arabic-First
SSR
App Router
01
Key Architecture Decisions
The 6 decisions that shaped the entire stack
Decision 01
Base UI over Radix for primitives
Radix maintenance slowed after WorkOS acquisition. Base UI (MUI team) has broader component coverage, active development, and shadcn now supports it with identical APIs.
Architecture
Decision 02
Custom JWT over NextAuth
API already handles auth. NextAuth adds unnecessary abstraction. Custom JWT with httpOnly cookies + Zustand memory store gives us full control over the token refresh lifecycle.
Security
Decision 03
3-layer state separation
Server state (TanStack Query) + client state (Zustand) + URL state (nuqs). No overlap, no confusion. Each layer has a single owner and clear boundaries.
Architecture
Decision 04
Ky over Axios for HTTP
Ky is 10x smaller (3KB vs 40KB), uses native fetch (works in server components), and has a cleaner interceptor API for the token refresh flow.
Bundle Size
Decision 05
Arabic-first with next-intl
Default locale is ar. RTL is the baseline, not an afterthought. shadcn CLI auto-converts logical properties. next-intl integrates natively with App Router and RSC.
UX / i18n
Decision 06
~50KB total bundle budget
Every package is justified and tree-shakeable. Heaviest: Motion (15KB), TanStack Query (12KB), RHF (8KB). No Moment.js, no Axios, no Swiper, no Redux.
Performance
02
Stack Overview
Every layer of the frontend — at a glance
LayerChoicePackage
FrameworkNext.js 15 (App Router)next
LanguageTypeScript 5.xtypescript
StylingTailwind CSS v4tailwindcss
UI Componentsshadcn/ui + Base UIshadcn@latest
Data FetchingTanStack Query v5@tanstack/react-query
Client StateZustandzustand
URL Statenuqsnuqs
FormsReact Hook Form + Zodreact-hook-form + zod
AuthCustom JWT (access + refresh)Custom middleware
i18nnext-intlnext-intl
AnimationMotion (Framer Motion)motion
IconsLucidelucide-react
MapsGoogle Maps@vis.gl/react-google-maps
ToastSonnersonner
Datesdate-fns + Hijridate-fns
HTTP ClientKyky
03
UI Components & Styling
Design system foundation with full RTL support
shadcn/ui (Base UI) UI
Copy-paste component system built on Tailwind + Base UI primitives. Full RTL support since Jan 2026.
Why Base UI over Radix: Actively maintained by MUI team, broader component coverage (combobox, multi-select), and shadcn now supports both with identical APIs.
tailwindcss v4 Core
Utility-first CSS framework. v4 introduces CSS-native config, automatic content detection, and native @theme support.
RTL setup: shadcn CLI transforms ml-4ms-4, text-lefttext-start automatically with --rtl flag.
lucide-react UI
Default icon library for shadcn/ui. 1500+ icons, tree-shakeable, consistent stroke style.
Alternative considered: @tabler/icons-react — more icons, but Lucide has tighter shadcn integration and smaller per-icon bundle.
embla-carousel-react UI
Lightweight carousel engine (~3KB). Used for: brand logos slider, testimonials, car image gallery, similar cars section.
Why not Swiper: Embla is ~3KB vs Swiper's ~40KB, and it's already a shadcn dependency.
sonner UI
Toast notification library. Elegant defaults, stacking, promise toasts, RTL-compatible. shadcn wraps it natively.
motion (Framer Motion) UI
Production-grade animation library. Page transitions, staggered list loading, bid success animation, countdown pulse, modal enter/exit.
04
Data Fetching & State
3-layer state separation — no overlap, no confusion
Server State
TanStack Query v5
Car listings
Bid data
User profile
Auction details
~ cached, revalidated, SSR-hydrated
Client State
Zustand
Auth tokens (memory)
Current user
Sidebar / modal state
Bid draft
~ ephemeral, UI-only
URL State
nuqs
Brand / model filter
City / fuel / seller type
Pagination
Search query
~ shareable, bookmarkable
@tanstack/react-query v5 Data
Server state manager. Caching, background revalidation, optimistic updates, request deduplication, SSR hydration.
Key patterns:
• Car listings → useQuery(['cars', filters]) with stale-while-revalidate
• Place bid → useMutation + optimistic update
• SSR prefetch → dehydrate in server components
• Auction → refetchInterval: 5000 for near-real-time
ky Data
Tiny (~3KB) HTTP client built on native fetch. Automatic retries, JSON parsing, interceptors for JWT tokens.
Why not Axios: 10x smaller, works in server components, cleaner interceptor API for token refresh.
ky.create({ prefixUrl, hooks }) auto-attaches access tokens, handles 401 → refresh → retry.
zustand Data
Minimal client state manager (~1KB). No boilerplate, no providers, works with SSR. Auth state, UI state only.
nuqs Data
Type-safe URL search params. Shareable URLs like /cars?brand=toyota&city=riyadh&page=2. Syncs with TanStack Query via queryKey.
05
Forms & Validation
Type-safe, performant forms with Arabic error messages
react-hook-form Form
Performant form library with minimal re-renders. Handles the 3-step sell flow with 20+ fields.
Forms: Registration, Login, Sell car (3 steps), Bid placement, Contact, Password reset, Profile edit.
zod Form
TypeScript-first schema validation. Define once, use for forms + API response parsing + type inference.
Shared schemas: carSchema, bidSchema, userSchema — reuse everywhere. Arabic errors via zod-i18n-map.
react-phone-number-input Form
International phone input with Saudi flag + +966 prefix, validation, formatting.
react-dropzone Form
Drag-and-drop file upload for profile avatar and car images in the sell flow.
input-otp Form
OTP input component (shadcn built-in). Phone verification during registration.
06
Authentication Flow
Custom JWT with secure token lifecycle
1
Login
User submits credentials
2
API Response
Returns access + refresh tokens
3
Store Tokens
Access in memory, refresh in httpOnly cookie
4
API Requests
Ky interceptor attaches token
5
401 Handling
Auto-refresh → retry, or redirect to login
middleware.ts Core
Next.js Edge Middleware for route protection. Checks refresh token cookie on protected routes.
Route classification:
Public: /, /cars, /cars/[id], /winners, /about, /faq, /contact
Auth pages: /login, /register (redirect away if logged in)
Protected: /profile, /my-cars, /my-bids, /sell-car, etc.
useRequireAuth Core
Client-side auth guard for actions on public pages. Guest clicks "Bid Now" → redirect to login with callbackUrl.
07
Translation & RTL
Arabic-first — RTL is the baseline, not an afterthought
next-intl Core
Full i18n for App Router. Server components support, ICU formatting, plurals, route-based locale switching.
Setup: Default ar, optional en. Route: /cars (Arabic) → /en/cars (English). Messages in messages/ar.json and messages/en.json.
zod-i18n-map Form
Arabic Zod error messages. "هذا الحقل مطلوب" instead of "Required".
DirectionProvider UI
Built into shadcn/ui. Wraps app with dir="rtl" so all primitives open in the correct direction.
date-fns + react-day-picker Util
Dates with Arabic locale, Hijri calendar, relative time ("قبل ساعتين"). react-day-picker supports arSA locale and RTL layout.
09
SEO & Discoverability
Every car listing indexed, rich, and shareable
generateMetadata() Core
Dynamic metadata for every car page. Generates unique title, description, and OG tags per listing. Car detail pages use generateMetadata({ params }) to fetch car data and set SEO tags server-side.
Critical for المثنى: Each car listing needs a unique title like "تويوتا كامري 2021 - مزاد المثنى" and description with price/mileage. Without this, all pages share the same generic title and Google won't rank them.
next/og (ImageResponse) Core
Auto-generate Open Graph images per car listing. When a car link is shared on WhatsApp or Twitter, it shows a branded card with the car photo, name, and price. Built into Next.js — no extra package needed.
WhatsApp sharing is massive in Saudi Arabia. A branded OG image with the car name and price dramatically increases click-through from shared links.
next-sitemap Core
Generates sitemap.xml and robots.txt automatically after build. Supports dynamic routes — crawls your car inventory API to include all /cars/[id] pages. Configurable priority and changefreq per route pattern.
For a site with thousands of car listings, Google needs a sitemap to discover and index them. Without it, new listings may take weeks to appear in search results.
schema-dts Core
TypeScript types for Schema.org structured data (JSON-LD). Use Vehicle or Product schema on car detail pages so Google can show rich results with price, image, seller name, and availability.
Rich results in Google: A car listing with structured data can show price, image, and seller directly in search results — higher click-through rate than plain blue links.
10
Error Handling
Graceful failures with Arabic user-facing messages
error.tsx per route group Core
Next.js App Router error boundaries. Create error.tsx in each route group — (auth), (protected), cars/[id], and root. Each shows an Arabic error message with a retry button. Root global-error.tsx catches layout-level errors.
Route-level error files:
app/[locale]/error.tsx — root fallback ("حدث خطأ غير متوقع")
app/[locale]/cars/[id]/error.tsx — car not found or API failure
app/[locale]/(protected)/error.tsx — protected route errors
app/[locale]/global-error.tsx — catastrophic layout failure
not-found.tsx Core
Custom Arabic 404 page. Shows "الصفحة غير موجودة" with a search bar and link back to homepage. Triggered by notFound() in server components when a car ID doesn't exist.
@sentry/nextjs DX
Production error tracking and performance monitoring. Captures client-side crashes, failed API calls, unhandled promise rejections with full stack traces, breadcrumbs, and user context.
Use for: Tracking bid submission failures, auth token refresh errors, image upload failures. Set up alerts for error spikes after deployments. Integrates with Next.js App Router and server components.
Centralized API error handling Core
Error type system in your ky instance. Map API error codes (400, 401, 403, 404, 422, 429, 500) to Arabic user-friendly messages. Handle network failures and timeouts with toast notifications via Sonner. Create a lib/errors.ts with typed error classes.
Error message mapping example:
• 401 → "انتهت صلاحية الجلسة، يرجى تسجيل الدخول مرة أخرى"
• 404 → "السيارة غير موجودة"
• 422 → Show field-level validation errors from API
• 429 → "عدد المحاولات كثير، حاول بعد قليل"
• Network error → "لا يوجد اتصال بالإنترنت"
11
Image Optimization
Fast car photos on every device and connection
next/image Core
Built-in image optimization. Automatic AVIF/WebP conversion, responsive sizing, lazy loading. The main car photo on the detail page must use priority prop (it's the LCP element). Use sizes prop for responsive breakpoints.
Car detail page strategy:
• Main image → priority, sizes="(max-width: 768px) 100vw, 60vw", blur placeholder
• Thumbnail strip → lazy loaded, sizes="80px"
• Car cards in listing → lazy loaded, sizes="(max-width: 768px) 100vw, 25vw"
• Configure remotePatterns in next.config.ts to allow your API's image domain
plaiceholder + sharp UI
Generate tiny blur placeholder images (LQIP) server-side for car photos. Shows a blurred preview while the full image loads — prevents layout shift and improves perceived performance.
Why it matters for cars: Car images are large (1-5MB originals). Without blur placeholders, users see a blank space for 1-3 seconds on mobile. With LQIP, they see a blurred preview instantly.
browser-image-compression Util
Client-side image compression before upload. Used in the sell-your-car flow when sellers upload 5-10 car photos. Reduces file size by 60-80% before hitting your API, saving bandwidth and upload time.
Sell flow optimization: Sellers on mobile upload photos from their camera (3-8MB each). Compress to max 1MB and resize to max 1920px width before uploading. Show a progress bar during compression.
12
Loading & Skeleton States
Instant feedback on every route transition
loading.tsx files Core
Next.js instant loading states on route transitions. Create loading.tsx in each route directory. Uses React Suspense under the hood — the loading UI shows immediately while the page's server component fetches data.
Where to add:
app/[locale]/cars/loading.tsx — skeleton grid of car cards
app/[locale]/cars/[id]/loading.tsx — skeleton of car detail layout
app/[locale]/(protected)/my-bids/loading.tsx — skeleton bid list
app/[locale]/(protected)/sell-car/loading.tsx — skeleton form
Skeleton components UI
shadcn's Skeleton component used to build loading variants for each content type. Build reusable skeletons: CarCardSkeleton, CarDetailSkeleton, BidHistorySkeleton, ProfileSkeleton. Place in components/skeletons/.
Already available in shadcn/ui — just run npx shadcn@latest add skeleton. Compose with Skeleton primitives matching your actual card/page dimensions for zero layout shift.
Suspense boundaries Core
Wrap non-critical sections in <Suspense> so the main content renders first. On the car detail page, the main info and bid form render immediately while "similar cars" and "map" stream in later.
Car detail page Suspense strategy:
• Immediate: car info, gallery, bid form (above the fold)
• Deferred via Suspense: map, similar cars, seller info, inspection report
• This improves TTFB and LCP significantly
14
Testing
Unit, component, and end-to-end test strategy
vitest DX
Fast unit test runner (Vite-powered). Use for testing: utility functions (price formatting, date formatting), Zod schemas, custom hooks (useRequireAuth, useCountdown), Zustand stores (auth store), and API service functions.
Alternative considered: jest — Vitest is faster, has native ESM support, and better TypeScript integration without extra config.
@testing-library/react DX
Component testing library. Test user interactions: form submission, filter selection, bid button behavior for guests vs authenticated users, dropdown menu items, RTL layout rendering.
playwright DX
End-to-end browser testing. Test critical user flows across real browsers. Must test RTL layout in both Chrome and Safari.
Critical E2E flows to test:
• Guest browses → clicks bid → redirected to login → logs in → redirected back → places bid
• Register with phone OTP → verify → complete profile
• Sell car: 3-step form completion → submit
• Filter cars by brand + city → pagination → click detail
• RTL layout: check no overlapping elements, correct text alignment
15
PWA & Mobile
Native-like experience for Saudi mobile users
manifest.json Util
Web App Manifest for "Add to Home Screen" functionality. Configure Arabic app name "المثنى", Saudi-themed icons (192x192, 512x512), theme color matching your navy primary, and display mode "standalone".
High impact in Saudi Arabia: Mobile web usage is dominant. Many Saudi users add frequently visited sites to their home screen. A proper manifest with icons makes المثنى look like a native app.
serwist (or next-pwa) Util
Service worker for offline caching. Cache viewed car listings so users can browse previously seen cars offline. Cache static assets (fonts, icons, CSS) for instant repeat visits.
Optional but recommended for mobile users on variable Saudi mobile connections. Start with asset caching only — don't try to make bid submission work offline.
16
Bundle Size Breakdown
~50KB gzipped total — every kilobyte justified
motion
~15KB
react-query
~12KB
react-hook-form
~8KB
base-ui
~8KB
nuqs
~6KB
phone-input
~5KB
google-maps
~5KB
next-intl
~4KB
ky + zustand + rest
~5KB
Total: ~50KB gzipped (excluding Next.js itself). For comparison, a typical Redux + Axios + Swiper + Formik stack would be ~120KB+ before you write a single component.
17
Project Structure
Feature-based organization with clear boundaries
src/ ├── app/ # Next.js App Router │ ├── [locale]/ # next-intl locale segment │ │ ├── layout.tsx # Root layout (fonts, providers, RTL) │ │ ├── page.tsx # Landing page (homepage) │ │ ├── cars/ # Public: car listings │ │ │ ├── page.tsx # Listing with filters │ │ │ └── [id]/ │ │ │ └── page.tsx # Car detail + bid │ │ ├── winners/ # Public: auction winners │ │ ├── (auth)/ # Auth route group │ │ │ ├── login/ │ │ │ ├── register/ │ │ │ ├── verify-otp/ │ │ │ └── forgot-password/ │ │ ├── (protected)/ # Protected route group │ │ │ ├── profile/ │ │ │ ├── my-cars/ │ │ │ ├── my-bids/ │ │ │ ├── transactions/ │ │ │ ├── notifications/ │ │ │ └── sell-car/ # 3-step multi-form │ │ ├── error.tsx # Root error boundary (Arabic) │ │ ├── not-found.tsx # Custom 404 page (Arabic) │ │ ├── global-error.tsx # Layout-level error fallback │ │ ├── loading.tsx # Root loading skeleton │ │ ├── sitemap.ts # Dynamic sitemap generation │ │ ├── robots.ts # Robots.txt configuration │ │ ├── about/ │ │ ├── contact/ │ │ └── faq/ │ └── api/ # Next.js API routes (BFF) │ └── auth/ │ ├── session/route.ts # Set httpOnly cookie │ └── refresh/route.ts # Refresh token proxy ├── components/ │ ├── ui/ # shadcn generated components │ ├── layout/ # Navbar, Footer, Sidebar │ ├── cars/ # CarCard, CarGallery, BidForm │ ├── auction/ # CountdownTimer, BidHistory │ ├── forms/ # PhoneInput, FileUpload, StepForm │ ├── skeletons/ # CarCardSkeleton, CarDetailSkeleton, etc. │ └── shared/ # Providers, ErrorBoundary ├── hooks/ # useRequireAuth, useCountdown, etc. ├── lib/ │ ├── api.ts # ky instance with interceptors │ ├── query-client.ts # TanStack Query client config │ ├── utils.ts # cn() helper, formatters │ ├── errors.ts # Error types + Arabic error messages │ ├── seo.ts # generateMetadata helpers, JSON-LD builders │ └── image.ts # Blur placeholder + compression utilities ├── stores/ │ └── auth-store.ts # Zustand auth store ├── schemas/ # Zod schemas (shared) │ ├── car.ts │ ├── bid.ts │ └── user.ts ├── services/ # API service functions │ ├── cars.ts # getCars, getCar, createCar │ ├── bids.ts # placeBid, getMyBids │ └── auth.ts # login, register, refresh ├── types/ # TypeScript types/interfaces ├── messages/ # next-intl translation files │ ├── ar.json │ └── en.json ├── middleware.ts # Auth + i18n middleware ├── tests/ # Vitest unit tests │ ├── components/ │ ├── hooks/ │ └── utils/ ├── e2e/ # Playwright E2E tests │ ├── auth.spec.ts │ ├── bid-flow.spec.ts │ └── sell-car.spec.ts └── public/ ├── manifest.json # PWA manifest └── icons/ # PWA icons (192x192, 512x512)
19
Complete Package Reference
Every dependency at a glance
CategoryPackagePurpose in Al MuthanaSize
FrameworknextApp Router, RSC, SSR, Middleware
StylingtailwindcssUtility-first CSS, RTL logical props0 runtime
UI Systemshadcn/ui20+ components (buttons, forms, cards, etc.)Copy-paste
Primitives@base-ui-components/reactHeadless accessible primitives~8KB
Iconslucide-react1500+ consistent stroke icons~0.5KB/icon
Carouselembla-carousel-reactImage gallery, brand slider, testimonials~3KB
ToastsonnerNotification toasts (bid success, errors)~3KB
AnimationmotionPage transitions, card hover, countdown pulse~15KB
Data@tanstack/react-queryServer state, caching, SSR hydration~12KB
HTTPkyAPI client with JWT interceptors~3KB
Client StatezustandAuth store, UI state~1KB
URL StatenuqsCar listing filters in URL~6KB
Formsreact-hook-formAll forms (register, sell, bid, contact)~8KB
ValidationzodSchema validation + type inference~2KB
Form Bridge@hookform/resolversZod → RHF integration~1KB
Phonereact-phone-number-input+966 Saudi phone input~5KB
Uploadreact-dropzoneCar images + avatar upload~3KB
OTPinput-otpPhone verification code input~2KB
Maps@vis.gl/react-google-mapsCar location on detail page~5KB
Datesdate-fnsFormatting, relative time, Arabic locale~2KB tree-shaken
Calendarreact-day-pickerDate picker (shadcn Calendar)~4KB
i18nnext-intlArabic/English translation, ICU messages~4KB
i18n Zodzod-i18n-mapArabic Zod error messages~1KB
Utilsclsx + tailwind-mergecn() helper for conditional classes~2KB
FontsNoto Sans Arabic + InterTypography (via next/font)Subsetting
SEOnext-sitemapAuto-generate sitemap.xml + robots.txtBuild-time
SEOschema-dtsType-safe JSON-LD structured dataTypes only
SEOnext/ogAuto-generate OG images per car listingBuilt-in
Imagesplaiceholder + sharpBlur placeholder generation (LQIP)Server-side
Imagesbrowser-image-compressionClient-side image compression before upload~4KB
Monitoring@sentry/nextjsError tracking + performance monitoring~15KB
Monitoringweb-vitalsCore Web Vitals real-user metrics~1.5KB
Analytics@next/third-partiesGoogle Analytics / GTM optimal loading~2KB
TestingvitestUnit + integration test runnerDev only
Testing@testing-library/reactComponent interaction testingDev only
TestingplaywrightEnd-to-end browser testingDev only
Bundle@next/bundle-analyzerBundle size visualizationDev only
PWAmanifest.jsonAdd to Home Screen supportConfig only
PWAserwistService worker for offline caching~3KB
20
Packages Considered & Rejected
What we evaluated and why we passed
PackageWhy NotUse Instead
axios40KB+, doesn't work in server componentsky (3KB, native fetch)
NextAuth / Auth.jsAPI already handles auth — adds unnecessary abstractionCustom JWT middleware
Redux / RTKOverkill. Server state = Query, client = Zustandzustand + react-query
SWRNo devtools, weaker mutation handling@tanstack/react-query
Radix UIMaintenance slowed after WorkOS acquisitionBase UI (MUI team)
Swiper40KB+ for carouselembla-carousel-react (3KB)
Moment.jsDeprecated, 70KB+, not tree-shakeabledate-fns
FormikSlower, more re-renders, weaker TSreact-hook-form
Ant Design / MUIOpinionated styles conflict with Figma designshadcn/ui
i18nextDoesn't integrate with App Router / RSCnext-intl
next-seoNext.js Metadata API is now built-in and more powerfulgenerateMetadata()
jestSlower, requires more config for ESM/TypeScriptvitest
cypressHeavier, slower than Playwright, less cross-browser coverageplaywright
workboxToo low-level for most PWA needsserwist (wraps workbox)