Designs hackathon-ready frontend architecture: folder layout, page shell, component boundaries, state flow, API touchpoints, TypeScript types, async UI states, and responsive patterns. Optimized for Next.js MVPs, speed, and demo quality. Use when scaffolding or refactoring UI, planning pages and components, wiring client to API, or when the user mentions frontend structure, layout, state management, or implementation order for an MVP.
Design a small, shippable frontend for one-day MVPs. Prefer boring patterns, one happy path, and UI that survives a live demo (loading, empty, error).
Pairs with contract-architect: define API/types first, then map UI to contracts.
lib/api, types, components — not a deep tree of abstractions."use client"lib/api helpersAdjust naming if the project uses Pages Router or a different data layer.
Use this as a template; delete unused folders rather than inventing new top-level concepts.
app/
layout.tsx # root shell: fonts, metadata, providers (minimal)
page.tsx # landing or main flow entry
(routes)/... # optional route groups
api/ # Route Handlers OR omit if 100% Server Actions
components/
ui/ # primitives: Button, Card, Input, Skeleton (shadcn-style or hand-rolled)
layout/ # AppShell, PageHeader, Sidebar (only if needed)
features/<feature>/ # domain: ReviewForm, RiskResultCard, SignalList
lib/
api.ts # fetch wrappers, base URL, error mapping
utils.ts # cn(), formatters
types/
index.ts # re-exports; or split api.ts types per domain file
public/
Rules
components/ui: reusable, mostly stateless; no domain imports from features.components/features/*: product language; may import ui and lib.types: shared DTOs aligned with backend; avoid duplicating shapes in random files.services/repositories/usecases unless the repo already uses them.Root layout.tsx
Feature page pattern
PageHeader: title + one-line subtitle (optional)max-w-3xl or max-w-4xl centered with horizontal padding for “SaaS” feelRegions
| Region | Purpose |
|---|---|
| Header | Product name, optional env badge |
| Main | Single primary task (review transaction) |
| Footer | Optional links; omit for hackathon |
Use one consistent page wrapper component (e.g. PageShell) if more than two pages exist.
Split by role, not by file count.
| Layer | Contains | Imports from |
|---|---|---|
ui/* | Button, Card, Badge, Input, Skeleton, Alert | React, styling utils only |
features/* | Forms, result panels, lists bound to domain | ui, lib, types |
layout/* | App shell, nav | ui |
Naming
*Section, *Panel, *Form — own local state and callbacks.*Card, *Row, *List — props in, markup out.Card-first UI
page.tsx.Default MVP
useEffect + useState for speed.useState or React Hook Form if validation is non-trivial.searchParams only when shareable links matter for the demo.Avoid for MVP
Data flow diagram (mental model)
User input → validate (client, light) → API (server) → typed response → UI state → cards
Error flow
lib/api → return { ok: false, error } or throw normalized ApiError.| Location | Responsibility |
|---|---|
lib/api.ts | baseUrl, fetch with JSON, timeout optional, error mapping |
| Route Handlers | app/api/.../route.ts — validate body, call DB/LLM, return DTO |
| Server Actions | Alternative: actions.ts with "use server" for mutations |
types | Request/response types matching contract-architect output |
Client call shape (conceptual)
submitReview(input: ReviewInput): Promise<ReviewResult>.network | validation | server | unknown.Security
Minimum set for a risk-review style MVP:
| Type / file | Purpose |
|---|---|
ReviewInput | Form payload / API request body |
ReviewResult | API success: recommendation, signals, explanation |
RiskSignal | Single flag: code, severity, message (as per contract) |
ApiErrorBody | { error: { code: string; message: string } } |
AsyncState<T> | Discriminated union for UI (see below) |
AsyncState pattern (discriminated union)
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "empty" } // optional: valid empty business result
| { status: "error"; message: string };
Use one pattern consistently: either AsyncState everywhere or separate booleans only in the smallest screen — not both.
Inference
z.infer<typeof Schema> if using Zod for forms; duplicate types only at API boundary if unavoidable.Every screen that depends on async data should implement:
| State | UI pattern |
|---|---|
| Loading | Skeleton in card shape; disable primary actions |
| Empty | Short message + optional CTA (“Enter a transaction to start”) |
| Success | Full card content; clear hierarchy |
| Error | Alert/banner + retry; do not wipe unrelated form fields |
Rules
< md; optional two-column md+ only if it improves the demo.h1 per view, section titles h2 or styled div).p-4–p-8); avoid edge-to-edge text on large phones.Execute in this order to maximize working demo time:
lib/api stub — return mock JSON matching types (optional but fast for UI).sm and md widths.Stop early if time is low: a working path with good error handling beats extra pages.
When “architecting,” produce a concise answer that covers:
| # | Topic |
|---|---|
| 1 | Folder tree (as applied to this repo) |
| 2 | Page layout — regions and main wrapper |
| 3 | Component list — ui vs features, main files |
| 4 | State flow — where data lives and updates |
| 5 | API touchpoints — files and function names |
| 6 | Types — key interfaces/unions |
| 7 | Async UI — how each state renders |
| 8 | Responsive — breakpoints and stacking rules |
| 9 | Implementation order — numbered steps |
lib/apicomponents/foo/bar/baz/widgets) without payoffPrioritize one flow: submit transaction context → show recommendation, signals, short explanation in cards. Keep navigation minimal; full-width readable content beats dense dashboards for judges.