5-Layer Architecture
Every Mukoko app enforces a strict 5-layer component hierarchy. Components import from the layer below, never sideways or upward. This ensures isolation, testability, and prevents cascading failures.
Layer Hierarchy
| Layer | Name | Path |
|---|---|---|
| L1 | Shared Primitives | components/ui/ |
| L2 | Domain Composites | components/weather/, components/reports/ |
| L3 | Page Orchestrators | components/landing/, components/dashboard/ |
| L4 | Error Boundaries + Loading States | components/section-error-boundary.tsx |
| L5 | Server Page Wrappers | app/[route]/page.tsx |
Imports flow downward only.
Live demonstration
This live demo shows all 5 layers in action. Each section is wrapped in a SectionErrorBoundary — click “Trigger error” to see how one section crashes without affecting the others.
28°C
Layer 1: Shared Primitives
Path: components/ui/
Foundational UI components from this registry — Button, Input, Card, Badge, etc. Installed via the shadcn CLI. Never import from higher layers.
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
import * as Slot from "radix-ui/internal/slot"
const buttonVariants = cva(
"inline-flex items-center justify-center ...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border-border bg-input/30",
},
size: {
default: "h-9 px-3",
sm: "h-8 px-3",
},
},
defaultVariants: { variant: "default", size: "default" },
}
)
export function Button({ className, variant, size, asChild, ...props }) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}Layer 2: Domain Composites
Path: components/weather/, components/reports/
Feature-specific components that compose Layer 1 primitives into domain-relevant UI. A WeatherCard combines Card + Badge + Progress from the registry.
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
export function ForecastCard({ location, temp, condition, confidence }) {
return (
<Card>
<CardHeader>
<CardTitle>{location}</CardTitle>
<Badge variant="outline">{condition}</Badge>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-foreground">{temp}°C</p>
<Progress value={confidence} className="mt-2" />
<span className="text-xs text-muted-foreground">
{confidence}% confidence
</span>
</CardContent>
</Card>
)
}Layer 3: Page Orchestrators
Path: components/landing/, components/dashboard/
Compose Layer 2 components into full page sections. Orchestrators NEVER hardcode rendering logic — they import and arrange domain composites.
import { ForecastCard } from "@/components/weather/forecast-card"
import { ActivityFeed } from "@/components/reports/activity-feed"
import { WeatherChart } from "@/components/charts/weather-chart"
export function WeatherDashboard({ forecasts, activities, chartData }) {
return (
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2 flex flex-col gap-4">
{forecasts.map((f) => (
<ForecastCard key={f.id} {...f} />
))}
</div>
<div className="flex flex-col gap-4">
<WeatherChart data={chartData} />
<ActivityFeed items={activities} />
</div>
</div>
)
}Layer 4: Error Boundaries + Loading States
Path: components/section-error-boundary.tsx
Every section gets wrapped in a SectionErrorBoundary. If a chart crashes, only that section shows an error — the rest of the page continues working. This is mandatory.
import { SectionErrorBoundary } from "@/components/section-error-boundary"
import { WeatherOverview } from "@/components/weather/overview"
import { ActivityFeed } from "@/components/reports/activity-feed"
import { WeatherChart } from "@/components/charts/weather-chart"
export function SafeDashboard({ data }) {
return (
<main className="flex flex-col gap-6">
<SectionErrorBoundary section="Weather Overview">
<WeatherOverview forecast={data.forecast} />
</SectionErrorBoundary>
<SectionErrorBoundary section="Activity Feed">
<ActivityFeed items={data.activities} />
</SectionErrorBoundary>
<SectionErrorBoundary section="Weather Charts">
<WeatherChart data={data.chartData} />
</SectionErrorBoundary>
</main>
)
}Layer 5: Server Page Wrappers
Path: app/[route]/page.tsx
Next.js App Router page.tsx files. Handle SEO metadata, data fetching, and pass props down to the orchestrator. These are React Server Components by default.
import { Metadata } from "next"
import { SafeDashboard } from "@/components/dashboard/safe-dashboard"
export const metadata: Metadata = {
title: "Dashboard — mukoko weather",
description: "Real-time weather intelligence for your region.",
}
async function getWeatherData() {
// Server-side data fetching
const res = await fetch("https://api.mukoko.com/weather", {
next: { revalidate: 900 }, // 15-minute cache
})
return res.json()
}
export default async function DashboardPage() {
const data = await getWeatherData()
return <SafeDashboard data={data} />
}Rules
- Components import from the layer below, never sideways or upward
- Each component is a standalone file — independently installable via the registry
- Page orchestrators NEVER hardcode rendering logic — they compose imported components
- All colors and styles come from CSS custom properties in globals.css
- SectionErrorBoundary wrapping is mandatory for every page section
- Server components by default — add “use client” only when using hooks or event handlers