Generating a Multi-Page E-Commerce Site from Scratch: A Hands-On Evaluation Method for AI Coding Tools
Generating a single function, a component, or even a static page with AI coding tools is now fairly easy. What actually separates the strong tools from the weak ones is a harder class of task: starting from an empty directory and producing, in one pass, an application with a dozen or more pages that installs, launches, and runs its core flow end to end. This article turns that task into a reproducible, scorable benchmark. It explains why a multi-page business site is a stronger evaluation target than a single-file demo, defines the task as verifiable engineering specifications (route table, domain types, directory conventions, and state ownership), and walks through a three-phase prompting strategy that makes generation splittable and checkable. It then analyzes the failure modes seen most often in testing, together with their root causes, and closes with an eight-dimension scoring checklist and a human-AI division of labor for shipping the result.
Using AI coding tools to generate a function, a component, or even a static page is fairly easy today. The tasks that truly separate the strong tools from the weak ones are of a different kind: starting from an empty directory and producing, in a single pass, an application with a dozen or more pages — home page, product detail, checkout, payment success, login and registration, and a user center (dashboard, orders, coupons wallet, settings) — that installs, launches, and runs its core flow end to end.
This task looks like just “generate a shopping site,” but it actually exercises several capabilities at once: how well the model follows front-end engineering conventions, cross-file consistency, how quickly it converges when fixing runtime errors, and how granularly a human developer can step in during the process. This article first defines the task as a reproducible, scorable evaluation, then breaks down the high-frequency failure modes seen in testing along with their root causes, and finally presents a workflow and acceptance criteria that significantly improve the “first-pass success rate.”
1. Why Use a Multi-Page Business Site as the Benchmark
A single-file demo carries very little information: there is no routing, no cross-page state, and no shared types, so the model can hardly go wrong. A multi-page business site is different. It inherently contains several groups of constraints that push against each other.
- Structural constraints: A dozen or more pages must hang off a single route tree, with consistent naming, directory hierarchy, and shared layout.
- State constraints: The shopping cart spans four pages (list, detail, checkout, payment success), and user identity spans login, registration, and the four sub-pages of the user center.
- Type constraints: Five core entities — product, cart line, order, user, and coupon — are referenced by nearly every page.
- Visual constraints: The layout must work at both desktop and mobile widths, while a single set of colors and spacing covers every page.
These four groups of constraints are tightly coupled: change one and the whole system moves. A single wrong field name on one page may only surface at type-check time or at runtime; one hard-coded color gets repeated ten times across ten pages. Precisely because of this, there is often a full round of rework between “it runs” and “the structure is sound, the style is consistent, and it is maintainable.” A fair evaluation looks not only at the first output, but also at where problems appear, how many rounds of human intervention are needed, and the final rework cost.
2. Defining the Task as Verifiable Engineering Specifications
Many “one-shot generation failures” stem from the task itself being defined too loosely. If you only say “build a shopping site with a certain front-end framework,” the model is free to improvise and the human has no basis for acceptance. A more reliable approach is to produce a specification first, then have the tool generate from it. The specification should cover at least the following elements.
1. Page list and route table
The route table is the “single source of truth” for how pages and data relate, and it should be fixed before any page code is written. The example below uses the /p/:id form for product identifiers, and the four sub-pages of the user center are handled with nested routes plus a shared layout.
/ Home · product list (filter, sort, pagination)/p/:id Product detail (images, specs, stock, add to cart)/checkout Checkout confirmation (address, coupon, amount summary)/checkout/success Payment success (order number, shipping estimate, continue shopping)/login /register Login / Register/user User center · dashboard/user/orders Order list (status filter)/user/wallet Coupons wallet (coupons, points)/user/settings Profile and preferences
2. Core data contracts
Describe entities with types first, and let all pages read and write data only through those types. Store amounts as integers in cents to avoid floating-point errors, and divide by 100 only when rendering in the front end. Below is a minimal contract that can be fed directly into generation:
// types/domain.ts — core domain model (contract first)export type ID = string;export interface Product {id: ID;title: string;priceCents: number; // always stored in cents to avoid floating-point errorsimages: string[]; // first image for lists, the rest for the detail gallerystock: number;tags: string[];detail: { specs: Record<string, string>; description: string };}export interface CartLine {productId: ID;qty: number;selected: boolean; // checkout only totals the checked items}export interface Coupon {code: string;type: 'fixed' | 'percent';value: number; // fixed: amount off in cents; percent: discount percentageminAmountCents: number; // minimum spend threshold}export interface Address { id: ID; receiver: string; phone: string; region: string; detail: string; }export type OrderStatus = 'paid' | 'shipped' | 'done';export interface Order {id: ID;items: CartLine[];amountCents: number; // computed and returned by the server; the front end must not change pricesstatus: OrderStatus;createdAt: string;}
3. Directory and layering conventions
Without conventions, every page invents its own file locations: one page puts components in components/, another puts them inside the page folder; one writes data directly in a component, another extracts a service. The result is a fragmented style that leaves later changes with nowhere to start.
src/├── routes/ # route table: the single mapping of pages to paths├── pages/ # one folder per page, page-level components only├── features/cart/ # cart state logic (independently testable, no view dependency)├── services/ # data access layer: api.ts (contract) + mock.ts (local fake data)├── components/ui/ # shared components like Button, Input, Price├── theme/tokens.ts # design tokens for colors, spacing, typography, radii└── types/domain.ts # domain model
The single most important rule here: page components must not reference fake data directly; they access data only through services/api.ts. Today that layer is backed by local fake data; tomorrow you swap in real endpoints by changing one place.
3. Prompt Strategy: Making Generation Splittable and Checkable
Asking for “generate all ten pages at once” is one of the highest-failure-rate approaches. The longer the generated output, the more conventions get missed, errors grow non-linearly with the number of files, and it is hard for a human to locate problems in one extremely long output. A safer approach is to proceed in three phases, each with a clear deliverable and acceptance criteria.
Phase 1: Produce only a plan, no page code.
You are a senior front-end engineer. The project directory is empty and needs a shopping site. First output only three parts: (1) the directory structure; (2) the route table; (3) the type definitions in
types/domain.ts. Also list the navigation relationships between pages, and where cart and login state are shared. Confirm before moving to the next step.
Phase 2: Scaffold and get one main flow working.
Following the confirmed structure, generate the route configuration and page placeholder components, ensuring that after installing dependencies the app starts and all ten routes are reachable. Then implement just one main flow: home → detail → add to cart → checkout → payment success. Put cart state in
features/cart, and compute checkout amounts centrally inservices. Writing hard-coded product data inside page components is prohibited in this phase.
Phase 3: Fill in the rest in dependency order, verifiable one batch at a time.
Fill in the following in order: login/registration → user center dashboard → orders page → coupons wallet → settings page. Persist login state in local storage; redirect unauthenticated users who access the user center to the login page, and return them to the original page after login. After each batch, run the type check and build, and report the results and fixes applied.
More effective than being “more detailed” is writing acceptance conditions directly into the prompt. For example: “After the build, every route under dist/ should produce a corresponding entry”; “no page may contain an image without an alt attribute”; “the contrast between body text and background must be at least 4.5:1.” Verifiable instructions can be verified, and they make it easier for the model to self-check.
4. Typical Failure Modes and Root-Cause Analysis
The problems that recur in testing can almost all be traced back to a missing convention. The table below groups them into seven typical patterns:
| Failure mode | Typical symptom | Root cause | Corresponding constraint |
|---|---|---|---|
| Structure drift | A same-named directory is created inside an existing one; files scatter across multiple places | No generation pre-step that fixes the directory tree | Phase 1 fixes the structure first |
| Hollow layering | The hooks/context folders exist, but the files contain no real logic | Generating directories first and filling in later, with no behavioral requirement | Define state ownership and behavior explicitly |
| Data coupling | Product and order fake data written directly into components | No agreed data access layer or replacement point | services/api.ts |
| Visual breakage | White-on-white buttons are invisible; the login page deforms on small screens | Colors hard-coded and scattered, missing design tokens and breakpoints | Tokens + responsive rules |
| Fails on first run | Missing dependencies, wrong import paths, runtime errors | Generation and verification never form a closed loop | Check and fix immediately after generation |
| Non-convergent fixes | Fix one problem, introduce two new ones | No regression check; changes lack boundaries | Fix tickets + full regression |
| Slow iteration | Each generation round takes long; trial-and-error is costly | Single task scope too large | Small commits, phase-by-phase acceptance |
4.1 Structure Drift: The Root Cause of Directory Chaos Is the Missing “Freeze” Step
The most common symptom is a tool creating another same-named directory inside the project directory, so the output is split across two places; some modules are imported from the outer level and some from the inner level, and it errors out on startup. This is not the model “disobeying,” but a problem of generation order: deciding where each file goes while generating, and drifting further off the path the longer generation continues.
The fix is to move the structural decision forward and freeze it: confirm the directory tree before writing the first line of code, after which any new file must land within the existing hierarchy; at the end of each batch, compare the actual directory tree against the agreed tree and clean up any extra levels immediately. This kind of check can be handed entirely to static analysis rules, such as limiting the number of levels in relative-path imports to avoid messy references like ../../../.
4.2 Hollow Layering: The Directory Exists, but the Behavior Does Not
Another common symptom is a tool that “understands” engineering conventions well, having created folders for state management, context, and custom hooks — but the files contain only default templates, while all business logic still piles up in the page components. The layering becomes decorative: it looks well-structured, but is actually harder to maintain than no layering at all, because later readers assume the logic has been consolidated and then have to dig back into the pages to find it.
The root cause is that the task described only “these directories must exist,” not “what behavior should live in these directories.” An effective approach is to provide a state ownership table that specifies, for each piece of state, where it is stored, who writes it, who reads it, and when it is invalidated:
State Stored in Written by Read byLogged-in user auth context + local login/register user center, headerstorage pagesCart cart context detail page, checkout page, header badgecart operationsSelected items selected flag on cart checkout page amount summarylinesCoupons services/coupons.ts — checkout page, coupons wallet
Also require each logic file to ship with a minimal, independently runnable test, such as “the total after adding to cart is computed correctly.” Logic that can be tested cannot stay in a hollow state.
4.3 Data Coupling: Writing Fake Data Straight into Components Is the Most Expensive Rework
Hard-coding a product array into the home-page component is the most common practice and also the costliest later: when integrating the real API, every page has to be modified one by one; and since different pages interpret the same product’s fields differently, discrepancies in price basis and stock checks easily creep in.
The correct approach is to declare a data access interface before generation and treat the fake data as just one implementation of it. Page code becomes completely unaware of “where the data comes from”:
// services/api.ts — data access contract (pages depend only on this)export interface ShopApi {listProducts(q: { keyword?: string; sort?: 'price_asc' | 'price_desc'; page: number }):Promise<{ items: Product[]; total: number }>;getProduct(id: ID): Promise<Product | null>;calcCartAmount(lines: CartLine[], coupon?: Coupon): Promise<{ subtotalCents: number; discountCents: number; payableCents: number }>;createOrder(input: { addressId: ID; lines: CartLine[]; couponCode?: string }): Promise<Order>;}// services/mock.ts — local implementation, swapped out as a whole when the API changesexport const mockApi: ShopApi = {async listProducts(q) {const filtered = q.keyword? products.filter(p => p.title.includes(q.keyword!)): products;const sorted = q.sort === 'price_asc'? [...filtered].sort((a, b) => a.priceCents - b.priceCents): q.sort === 'price_desc'? [...filtered].sort((a, b) => b.priceCents - a.priceCents): filtered;return { items: sorted, total: sorted.length };},// other methods are implemented analogously, omitted here};
With this layer of isolation, the problem of “fake data coupling into code” disappears at its source.
4.4 Visual Breakage: White-on-White Text and Mobile Deformation
White text on a white button, so the text is invisible, is a very typical generation defect. The root cause is that colors are hard-coded as scattered hex values across components, so the model cannot perceive how they look when combined; when one button is changed, other components keep using the old color values. A login page that gets squeezed and deforms on small screens is the result of missing breakpoint and flex-layout constraints.
The fix is to consolidate visual decisions into design tokens and have all components reference only the tokens. Tokens expressed as objects are easy to check statically — for example, forbidding color literals beginning with # from appearing in component files:
// theme/tokens.tsexport const colors = {brand: '#2B5AED',textPrimary: '#1A1A1A', // contrast with white is about 16:1textOnBrand: '#FFFFFF', // contrast with brand is about 5.9:1, meeting the 4.5:1 thresholdsurface: '#FFFFFF',border: '#E5E7EB',danger: '#DC2626',} as const;export const spacing = { xs: 4, sm: 8, md: 16, lg: 24, xl: 40 } as const;
One clarification: the contrast numbers here are computed for the example color values, so if you use your own palette, go by your actual calculations. But the method itself is directly reusable: when the primary brand color combined with white does not reach 4.5:1, do not use white text — switch to a darker text color or darken the background. Putting the contrast requirement into the prompt is far less effort than checking screenshots one by one after the fact.
4.5 Fails on First Run and Non-Convergent Fixes
Generated code erroring on its first run is the norm, not the exception. What actually determines efficiency is not “whether it errors” but “whether the errors converge.” Recurring error types include: importing without declaring the dependency, inconsistent path casing, async data with no handling for loading and empty states, and types not matching the implementation.
A practical rule is: each fix round handles only one class of error and requires a full regression after the fix. Otherwise oscillation — “fix one, introduce one” — is easy.
Fix record template- Symptom: (paste the full error message, including file and line number)- Category: missing dependency / wrong path / type mismatch / runtime exception / style- Root cause: (one sentence)- Change scope: (list the files modified)- Regression result: type check pass/fail, build pass/fail, main flow pass/fail
5. Making “First-Pass Success Rate” Measurable: An Eight-Dimension Scoring Checklist
A subjective sense of “how usable it is” cannot guide improvement. It is better to use a fixed task, fixed prompts, and a fixed acceptance script, and score the following eight dimensions (each on a scale of 1 to 5):
| Dimension | Observation point | How to judge |
|---|---|---|
| First-run usability | How many rounds of human fixing are needed from zero to a reachable home page | Record fix rounds and error types |
| Structural consistency | Whether directories, naming, and layering match the conventions | Compare the actual directory tree with the agreed tree |
| Layering effectiveness | Whether state logic has real, testable behavior | Check test coverage and the number of hollow files |
| Data decoupling | Whether fake data is concentrated in the data access layer | Search page components for hard-coded arrays |
| Visual consistency | Whether colors and spacing are unified and contrast meets the standard | Token reference rate + contrast calculation |
| Responsive quality | Layout behavior at desktop and mobile widths | Screenshot comparison at fixed breakpoints |
| Error convergence speed | Errors fixed per unit time, and whether new ones are introduced | Fix-record statistics |
| Maintainability | How many files must change to add a new page | Actual change count |
The value of this table is that it breaks “feels unusable” into specific, locatable defects. For example, the difference between two evaluations might come entirely from “data decoupling” and “error convergence speed,” while first-run usability shows no difference. That is enough to adjust your prompts and workflow, rather than just swapping the tool.
6. Turning AI Output into a Deliverable: Dividing Labor Between Human and AI
Generation is only the starting point. A shopping site that can actually ship still needs a set of things that AI struggles to judge on its own. A reasonable division of labor:
- AI handles: page skeletons, route configuration, type definitions, shared components, form validation rules, fake data and local mocks.
- Humans handle: domain modeling trade-offs (such as coupon rules and when to deduct stock), security boundaries, accessibility and performance budgets, real API integration, and final acceptance.
A few bottom lines deserve special attention:
- No front-end validation is a security boundary. Prices, stock, and discounts must be computed and returned by the server; the front end is only responsible for display. Putting “compute the amount in the front end and then submit” into generated output is a typical security defect.
- Credentials must not enter front-end code. API keys and signing private keys must not appear in front-end source or the repository; inject them via environment variables at build time, or route the calls through a server-side proxy.
- Handle output and input differently. Escape user input before rendering, and run rich text through a whitelist filter; validate route parameters and query parameters for type and range as well.
- Verify accessibility in parallel. Images must have alternative text, form controls must have associated labels, interactive elements must be operable by keyboard, and dynamic announcements must be recognized by assistive technology.
7. Conclusion
“Generating an e-commerce site from zero to one” is an excellent evaluation task because it exposes the weakness of AI coding tools that is most easily hidden: not that they cannot write pages, but that they cannot write a system that is structurally consistent, runnable, and maintainable. The problems that recur in testing — structure drift, hollow layering, data coupling, visual breakage, and non-convergent errors — can overwhelmingly be solved by engineering conventions, not by “trying a few more times.”
Three practical recommendations:
- Spec first, code second: finalize the four essentials — route table, domain types, directory conventions, and state ownership table — before starting generation.
- Skeleton first, details second: generate in batches with per-batch acceptance, breaking a long task into several small, regressable ones.
- Testable first, ship second: give every module a minimal test and automated checks, so AI output can be verified by machine rather than by a human scrolling through page after page.
With these three points, the role of AI coding tools shifts from “a lucky code generator” to “an executor that follows engineering conventions.” The criterion for choosing a tool should therefore also shift, from “can it generate” to “can the output pass your acceptance standards.”
