Generating the React Fetch Layer with a Builder Agent
In this lab you will direct Claude Code, working in the AI as Builder role, to generate the complete data-fetching layer of a React client from a documented API contract, while you take the Engineering Manager role: scoping the task, reviewing the diff, auditing the generated code against the contract, testing it against a live backend, and requiring at least one revision before you approve the work. This mirrors how professional teams ship code with agentic tools: the agent types, and the engineer owns quality.
Estimated time: 45 minutes
Scenario
In Lesson 3, you designed a RESTful API for TaskFlow, a project management application. Your tech lead has since approved version 1 of the Tasks endpoints, and a teammate has implemented the backend, which is provided in full below. The frontend work now falls to you, with one twist: engineering leadership is piloting agentic AI tools, and your assignment is to have an AI Builder generate the entire client fetch layer while you review it the way a manager reviews a pull request.
You may not write the client code by hand. Your value in this lab is judgment: verifying that generated code matches the contract exactly, parses errors correctly, and holds up when the network misbehaves.
Roles in This Lab
- Claude Code (Builder): writes every line of client code.
- You (Engineering Manager): provide the API contract, set boundaries in
CLAUDE.md, review thegit diff, audit the code against a checklist, run functional tests, request at least one revision, and approve commits.
Part 1: Set Up the TaskFlow Backend and Client Shell (about 10 minutes)
Everything in this part is provided; you only paste, install, and run.
-
Create the project folder and initialize a Git repository:
mkdir taskflow-fetch-lab cd taskflow-fetch-lab git init -
Create a
.gitignorefile at the project root:.gitignorenode_modules/ .env -
Create the backend. First, the package manifest:
server/package.json{ "name": "taskflow-api", "version": "1.0.0", "private": true, "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "cors": "^2.8.5", "express": "^4.19.2" } } -
Next, the server itself. This implements the standardized success and error envelopes you studied in Lesson 3, with an in-memory data store so no database setup is required:
server/server.jsconst express = require("express"); const cors = require("cors"); const { randomUUID } = require("crypto"); const app = express(); const PORT = process.env.PORT || 5000; // The Vite development server runs on port 5173 by default. app.use(cors({ origin: "http://localhost:5173" })); app.use(express.json()); let tasks = [ { id: randomUUID(), title: "Draft the v1 endpoint reference", description: "Write the endpoint table for the Tasks resource.", priority: "high", status: "done", dueDate: "2026-08-21", assignee: "Priya", }, { id: randomUUID(), title: "Review the error envelope with the team", description: "Confirm every route returns the standard error object.", priority: "medium", status: "in-progress", dueDate: "2026-08-28", assignee: "Marcus", }, { id: randomUUID(), title: "Plan the projects resource", description: "Outline CRUD routes for projects before the next sprint.", priority: "low", status: "todo", dueDate: null, assignee: null, }, ]; const success = (data) => ({ status: "success", data }); const failure = (message, code) => ({ status: "error", error: { message, code }, }); const UPDATABLE_FIELDS = [ "title", "description", "priority", "status", "dueDate", "assignee", ]; app.get("/api/v1/tasks", (req, res) => { res.status(200).json(success(tasks)); }); app.get("/api/v1/tasks/:id", (req, res) => { const task = tasks.find((item) => item.id === req.params.id); if (!task) { return res .status(404) .json( failure("The requested task could not be found.", "RESOURCE_NOT_FOUND") ); } res.status(200).json(success(task)); }); app.post("/api/v1/tasks", (req, res) => { const body = req.body || {}; if (typeof body.title !== "string" || body.title.trim() === "") { return res .status(400) .json(failure("A non-empty title is required.", "VALIDATION_ERROR")); } const task = { id: randomUUID(), title: body.title.trim(), description: typeof body.description === "string" ? body.description : "", priority: ["low", "medium", "high"].includes(body.priority) ? body.priority : "medium", status: ["todo", "in-progress", "done"].includes(body.status) ? body.status : "todo", dueDate: body.dueDate || null, assignee: body.assignee || null, }; tasks.push(task); res.status(201).json(success(task)); }); app.put("/api/v1/tasks/:id", (req, res) => { const index = tasks.findIndex((item) => item.id === req.params.id); if (index === -1) { return res .status(404) .json( failure("The requested task could not be found.", "RESOURCE_NOT_FOUND") ); } const body = req.body || {}; if ( "title" in body && (typeof body.title !== "string" || body.title.trim() === "") ) { return res .status(400) .json(failure("A non-empty title is required.", "VALIDATION_ERROR")); } for (const field of UPDATABLE_FIELDS) { if (field in body) { tasks[index][field] = field === "title" ? body.title.trim() : body[field]; } } res.status(200).json(success(tasks[index])); }); app.delete("/api/v1/tasks/:id", (req, res) => { const index = tasks.findIndex((item) => item.id === req.params.id); if (index === -1) { return res .status(404) .json( failure("The requested task could not be found.", "RESOURCE_NOT_FOUND") ); } tasks.splice(index, 1); res.status(200).json(success(null)); }); app.use((req, res) => { res .status(404) .json( failure("The requested resource could not be found.", "RESOURCE_NOT_FOUND") ); }); app.listen(PORT, () => { console.log(`TaskFlow API listening on http://localhost:${PORT}`); }); -
Install the backend dependencies and start the server:
cd server npm install npm startOpen
http://localhost:5000/api/v1/tasksin your browser. You should see the success envelope containing the three seed tasks. Leave this terminal running for the rest of the lab. -
Create the API contract document. This file is the context you will feed to the Builder agent, and it is the source of truth for your audit later:
docs/api-schema.md# TaskFlow API Contract (v1) Base URL: the value of the client environment variable `VITE_API_URL` (during local development, `http://localhost:5000`). All endpoints are versioned under `/api/v1`. All request and response bodies use JSON. ## Response Envelopes Every successful response uses this shape: ```json { "status": "success", "data": "..." } ``` Every error response uses this shape: ```json { "status": "error", "error": { "message": "Human-readable explanation.", "code": "MACHINE_READABLE_CODE" } } ``` ## The Task Resource | Field | Type | Notes | | ----------- | -------------- | ---------------------------------------------------- | | id | string | Server-generated UUID. Read only. | | title | string | Required. Must not be empty. | | description | string | Optional. Defaults to an empty string. | | priority | string | One of low, medium, high. Defaults to medium. | | status | string | One of todo, in-progress, done. Defaults to todo. | | dueDate | string or null | ISO date string, for example 2026-09-01. | | assignee | string or null | Name of the assigned teammate. | ## Endpoints | Method | Path | Request Body | Success Response | Error Responses | | ------ | ----------------- | --------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------------- | | GET | /api/v1/tasks | none | 200, data: array of tasks | none | | GET | /api/v1/tasks/:id | none | 200, data: one task | 404 RESOURCE_NOT_FOUND | | POST | /api/v1/tasks | JSON object with title (required) plus any optional task fields | 201, data: the created task | 400 VALIDATION_ERROR | | PUT | /api/v1/tasks/:id | JSON object with any subset of title, description, priority, status, dueDate, assignee | 200, data: the updated task | 400 VALIDATION_ERROR, 404 RESOURCE_NOT_FOUND | | DELETE | /api/v1/tasks/:id | none | 200, data: null | 404 RESOURCE_NOT_FOUND | Any other path returns 404 with the code RESOURCE_NOT_FOUND. -
In a second terminal, scaffold the React client with Vite from the project root, then install its dependencies:
npm create vite@latest client -- --template react cd client npm install -
Create the client environment files. The real
.envstays out of version control (the root.gitignorealready excludes it); the.env.examplecopy documents the required configuration for reviewers:client/.envVITE_API_URL=http://localhost:5000client/.env.exampleVITE_API_URL=http://localhost:5000NoteLesson 2 demonstrates client environment variables with the
REACT_APP_prefix, which belongs to the older Create React App toolchain. This lab uses Vite, the current standard build tool for React projects. Vite exposes only variables prefixed withVITE_, and client code reads them throughimport.meta.envinstead ofprocess.env. -
Create
CLAUDE.mdat the project root. Claude Code reads this file at the start of every session, which makes it the reliable place to enforce the Builder role and its boundaries:CLAUDE.md# TaskFlow Client Project Instructions You are acting as the Builder agent for this project. The human developer is your Engineering Manager and reviews every change before it is committed. ## Boundaries - Only create or modify files inside `client/src`. - Never modify anything in `server/`, `docs/`, or this file. - Do not install additional dependencies. Use the browser `fetch` API, not axios. - The API contract in `docs/api-schema.md` is the source of truth. Do not invent endpoints, fields, or response shapes that it does not document. - Read the API base URL from the Vite environment variable `VITE_API_URL` through `import.meta.env`. Never hardcode `http://localhost:5000` in any source file. -
Make your initial commit so the Builder’s work will appear as a clean diff:
cd .. git add -A git commit -m "chore: initial setup with TaskFlow API, schema, and client shell"
Part 2: Direct the Builder (about 15 minutes)
If you do not have access to a Pro-level Claude account through Per Scholas, you may complete this lab using the free tier of Claude or another AI assistant with equivalent capabilities. Free-tier usage limits may require you to complete the lab across more than one session.
Fallback without Claude Code: open a chat at https://claude.ai , paste the full contents of docs/api-schema.md, and request the same four file changes described in the Builder directive below. Create each file manually by pasting the generated code into your editor; for this fallback path only, pasting code by hand is permitted. If usage limits interrupt you, reduce the scope to two functions (getTasks and createTask) and record the reduction in CODE_REVIEW.md.
-
Install Claude Code if you have not already (Node.js 18 or later is required, which you already have):
npm install -g @anthropic-ai/claude-code -
From the project root (
taskflow-fetch-lab), start Claude Code and log in with your Per Scholas-provided Claude account when prompted:claudeIf you are not logged in, run
/logininside the session. -
Paste the following Builder directive as your prompt. This is the entire scope of the task; the agent should need nothing else beyond the contract file:
Read docs/api-schema.md. It is the complete API contract for the TaskFlow backend. Acting as the Builder, create the client fetch layer: 1. client/src/api/client.js: a reusable fetch wrapper. It must read the base URL from import.meta.env.VITE_API_URL, send a Content-Type: application/json header on requests that have a body, parse JSON responses, and convert any non-2xx response into a thrown Error whose message comes from the standard error object documented in the schema, falling back to the HTTP status when the body cannot be parsed. 2. client/src/api/tasks.js: one exported function per documented endpoint (getTasks, getTask, createTask, updateTask, deleteTask), each using the wrapper and matching the documented method, path, and request body exactly. Each function must return the unwrapped data property from the success envelope. 3. client/src/components/TaskList.jsx: a component that loads tasks on mount and clearly renders all three request states (loading, error, success). Include a form to create a task (title and priority), a button on each task that marks it done through updateTask, and a delete button on each task. Do not add client-side validation for the title; let the server validate it, and display the server's error message in the UI when a request fails. 4. Replace client/src/App.jsx so it renders TaskList. Do not modify any other files. When you are done, list the files you created or changed and summarize how the error handling works. -
Claude Code asks permission before creating or editing files. Read each request and approve deliberately; this review habit is part of the learning objective. If the agent asks a clarifying question, answer it using
docs/api-schema.mdas the source of truth. Do not write or edit any client code yourself. -
When the agent reports it is done, open a new terminal and start the client from the project root. Keep the backend running in the first terminal, and leave the Claude Code session open in the second, because you will return to it for the revision in Part 3:
cd client npm run devOpen
http://localhost:5173in your browser.
The provided server allows requests only from http://localhost:5173. If Vite starts on a different port because 5173 is busy, stop the process using that port, or update the origin value in server/server.js. Editing the provided backend configuration is part of setup and does not violate the Builder rule, which applies to the client code.
Part 3: Audit the Work as Engineering Manager (about 15 minutes)
The Builder is fast, but you are accountable. In this part you review the diff, audit the code against the contract, test it against the live backend, and require at least one revision.
Step 1: Review the Diff
New files do not appear in a plain git diff, so stage everything first and review the staged diff (or use the Source Control panel in VS Code):
git add -A
git diff --stagedRead every generated file. You are looking for contract violations, not style preferences.
Step 2: Work Through the Audit Checklist
Record a pass or fail for each item, with evidence, in CODE_REVIEW.md (template below).
| # | Check | What to look for |
|---|---|---|
| 1 | Contract fidelity | Every function in client/src/api/tasks.js uses the exact method, path, and request body documented in docs/api-schema.md; nothing is invented. |
| 2 | Error parsing | The wrapper throws on any non-2xx response and takes the message from the standard error object, not from a generic status text. |
| 3 | Configuration | The base URL comes from import.meta.env.VITE_API_URL; no hardcoded http://localhost:5000 appears anywhere in client/src. |
| 4 | Envelope unwrapping | The api functions return the data property of the success envelope, so components never dig through status themselves. |
| 5 | Request states | Loading, error, and success states all render, and the error state shows the thrown message. |
| 6 | Headers | A Content-Type: application/json header is sent on requests that have a body. |
| 7 | Boundaries | Only files inside client/src were created or changed. |
Step 3: Run the Functional Tests
With the backend and client both running, exercise every operation:
| # | Test | How | Expected result |
|---|---|---|---|
| 1 | Seed data loads | Open http://localhost:5173 | A brief loading state, then the three seed tasks. |
| 2 | Create | Submit the form with a title | The new task appears in the list. |
| 3 | Update | Use the mark-done button on a task | The task’s status changes to done. |
| 4 | Delete | Use the delete button on a task | The task disappears from the list. |
| 5 | Server validation surfaces | Submit the form with an empty title | The server’s message “A non-empty title is required.” appears in the UI. |
| 6 | Not-found error | Open http://localhost:5000/api/v1/tasks/not-a-real-id in the browser | The raw standard error JSON with the code RESOURCE_NOT_FOUND. |
| 7 | Network failure | Stop the server with Ctrl+C, then reload the client | The error state renders instead of a blank page or crash. Restart the server. |
Step 4: Record Findings and Approve the Baseline
Create CODE_REVIEW.md at the project root using this template, fill in the checklist and test results, then commit the reviewed work:
# Code Review: AI-Generated Fetch Layer
## Reviewer
Name and date.
## What the Agent Generated
One short paragraph describing the files and the overall approach.
## Audit Checklist Results
| # | Check | Pass or Fail | Evidence |
| - | ----- | ------------ | -------- |
## Functional Test Results
| # | Test | Result |
| - | ---- | ------ |
## Defect or Improvement Identified
What you found, where it is, and why it matters.
## Revision Requested
The exact prompt you gave the agent.
## Verification of the Fix
What the new diff showed and which tests you re-ran.
## Reflection
Two or three sentences: where did the agent save you time, and where was
your review essential?git add -A
git commit -m "feat: AI-generated fetch layer (reviewed)"Step 5: Require a Revision
You must send the agent back at least once. If your audit found a real defect (a wrong path, a missed error case, a hardcoded URL), use that. If the code passed every check, choose one required improvement instead. Good candidates:
- Add an
AbortControllerto the mount fetch inTaskList.jsxso the component cannot set state after it unmounts. - Render a clear empty-state message when the task list contains no tasks.
- Disable the form and buttons while a request is in flight to prevent duplicate submissions.
Give the agent a specific, scoped prompt describing exactly what to change. Because you committed the baseline in Step 4, a plain git diff now shows only the revision. Review the diff, re-run the affected functional tests, record the outcome in CODE_REVIEW.md, and commit:
git add -A
git commit -m "fix: revision requested during engineering review"Part 4: Publish Your Repository (about 5 minutes)
-
Create a new public repository on GitHub named
taskflow-fetch-lab. -
Connect it and push:
git remote add origin https://github.com/YOUR-USERNAME/taskflow-fetch-lab.git git branch -M main git push -u origin main
Submission Guidelines
Submit the link to your GitHub repository on Canvas. Your repository must contain:
server/with the provided TaskFlow API exactly as given.docs/api-schema.md, the contract fed to the agent.CLAUDE.mdwith the Builder boundaries.client/with the AI-generatedsrc/api/client.js,src/api/tasks.js,src/components/TaskList.jsx, the updatedsrc/App.jsx, and.env.example.CODE_REVIEW.mdwith your completed checklist, test results, revision prompt, and verification.- A commit history that separates setup, the reviewed generation, and the revision.
If conversation sharing is enabled in your workspace, you may additionally paste a shared link to your Claude Code or claude.ai conversation into CODE_REVIEW.md as supporting evidence. If sharing is disabled, the prompts recorded in CODE_REVIEW.md are sufficient.
Grading
This lab is worth 30 points. Your work will be graded based on the following criteria:
| Criteria | Description | Points |
|---|---|---|
| Working Fetch Layer | The AI-generated client runs against the provided backend: tasks load, create, update, and delete operations succeed, and the error states render correctly. | 10 |
| Engineering Manager Audit | CODE_REVIEW.md shows a completed checklist with specific evidence, at least one identified defect or required improvement, the exact revision prompt given to the agent, and verification of the fix. | 15 |
| Repository Completeness and Professionalism | The repository contains all required artifacts with a clean structure and a commit history that separates setup, generation, and revision. | 5 |
| Total | 30 |