Step 1: Understand server and client components
In Next.js App Router, every component is a server component by default. Server components run on the server, fetch data directly, and send rendered HTML to the browser. The user gets a fully built page without waiting for JavaScript to load and execute.
Client components are different. You mark them with "use client" at the top of the file. They run in the browser and handle interactivity: form inputs, click handlers, local state. The browser downloads the JavaScript, then hydrates the HTML the server already sent.
This split is an architectural trade-off you make per page. The inventory dashboard shows data from the database. That data changes when Marco processes cacao, not in real time. A server component is right for this page -- it fetches data on the server and sends finished HTML. Fast initial load, no client-side JavaScript needed for the data.
The order form is different. The user selects products, enters quantities, sees validation errors, and submits. That interactivity requires state and event handlers -- it must be a client component.
When you direct Claude to build pages, specify this upfront. "Use a server component for the inventory dashboard" and "Use a client component for the order form" are constraints that prevent Claude from picking whichever pattern it defaults to. If you don't specify, you'll spend time undoing architectural decisions later.
Step 2: Build the inventory dashboard
Direct Claude to build the dashboard page:
Build the inventory dashboard page at app/page.tsx as a server component. Fetch data from the API: all inventory grouped by processing stage (fermenting, drying, roasting, finished) and by farm. Show farm name, cacao variety, batch count, and available product count for finished batches. Use Tailwind CSS for styling. Reference the CLAUDE.md governance file for ticket details.
Document the decision. Add a comment at the top of the dashboard file explaining why it's a server component: the data is fetched once per request, there's no client-side interactivity, and rendering on the server avoids sending the Prisma client to the browser. This is professional communication -- a future developer or AI session that opens this file needs to understand the constraint, not guess at it.
After the page renders, check the data against what's in the database. The API contract promised a specific response shape. If the dashboard shows "undefined" where a farm name should be, or shows zero products when the seed data has fifteen, the contract between the API and the frontend is broken. The server returned 200 -- no error in the logs. The failure is silent.
Step 3: Build the order form
Direct Claude to build the order creation page:
Build the order creation page at app/orders/new/page.tsx as a client component. Fetch available products from the API on mount. Show each product with its current available stock. Include fields for customer name and country. Validate required fields on submit. POST to /api/orders with the selected products. After a successful order, redirect to the order status page.
The form needs validation that helps Marco's staff complete the task. Required field indicators, immediate error messages when a field is empty or a quantity exceeds available stock, and a disabled submit button until the form is valid. These are decisions about user experience, not just error handling.
Step 4: Review form validation
Check what Claude generated. AI commonly builds validation that catches empty fields but misses edge cases real users hit.
Does the form reject whitespace-only customer names? A field containing only spaces passes a simple !== "" check but is effectively empty. Does the quantity input accept zero or negative numbers? What happens when the user fills the form, gets a validation error, clicks the back button, and returns -- does the form state persist or does it reset to empty?
These gaps exist because AI generates validation based on the expected path, not the messy paths real users take. When you find a gap, direct Claude to fix it with a specific constraint: "Trim whitespace before validating the customer name. Reject names that are empty after trimming."
Step 5: Build the order status page
Direct Claude to build the order status page:
Build the order status page at app/orders/page.tsx. The page layout is a server component that fetches all orders from GET /api/orders. Each order shows: customer name, country, status, and the products allocated to it. The status field is a dropdown that lets Marco update the order status (confirmed, in production, shipped, delivered) -- this dropdown is a client component nested inside the server-rendered page.
This page has both server and client components. The order list renders on the server. The status dropdown handles interactivity in the browser. The student experiences the server/client boundary within a single page -- some parts render once on the server, other parts stay interactive in the browser.
After building, share a screenshot of the inventory dashboard with Marco. His response:
Good. Can we also track the flavour profiles per batch? My shop in Portland orders based on tasting notes, not bar names.
This is a natural extension. Marco sees the system working and remembers something his customers care about. The change is small -- a flavour_profile text field on the batches table and a display column on the dashboard. Write a migration to add the field, update the seed data to include flavour profiles for finished batches, and update the dashboard.
Note what this involves: a new migration (schema change), a seed data update (data change), an API response change (contract change), and a frontend display change (UI change). A small request from the client touches all three layers. This is what full-stack means in practice.
Step 6: Check for hydration mismatches
The server renders HTML. The client hydrates it -- attaching event handlers to the HTML the server already built. If the server and client disagree on what the HTML should look like, that's a hydration mismatch.
Common causes: Date.now() returns a different timestamp on the server than the client. Math.random() returns a different value. Browser-only APIs like window.innerWidth don't exist on the server. AI generates these patterns because it doesn't track which code runs where.
Open the browser console. Look for hydration warnings. If you see any, trace them back to the component that causes the disagreement. Direct Claude to fix it:
Audit all components for hydration mismatches. Check for Date.now(), Math.random(), window.*, document.*, or any browser-only API used in server components or during server-side rendering. Move any client-dependent logic inside useEffect or mark the component as a client component.
Hydration mismatches degrade gracefully in development -- you see a warning in the console but the page still works. In production, they cause unpredictable behavior: content flashing, event handlers attaching to the wrong elements, state corruption. A clean console in development means a safer deployment.
Check: Visit the inventory dashboard -- it shows inventory by stage and farm. Open the order form -- available products display with current stock. Submit an order -- the API creates it and the inventory updates. Check the browser console for hydration warnings.