Skip to Content
Lab 3 (AI)

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 the git 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.

  1. Create the project folder and initialize a Git repository:

    mkdir taskflow-fetch-lab cd taskflow-fetch-lab git init
  2. Create a .gitignore file at the project root:

    .gitignore
    node_modules/ .env
  3. 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" } }
  4. 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.js
    const 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}`); });
  5. Install the backend dependencies and start the server:

    cd server npm install npm start

    Open http://localhost:5000/api/v1/tasks in your browser. You should see the success envelope containing the three seed tasks. Leave this terminal running for the rest of the lab.

  6. 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.
  7. 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
  8. Create the client environment files. The real .env stays out of version control (the root .gitignore already excludes it); the .env.example copy documents the required configuration for reviewers:

    client/.env
    VITE_API_URL=http://localhost:5000
    client/.env.example
    VITE_API_URL=http://localhost:5000
    Note

    Lesson 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 with VITE_, and client code reads them through import.meta.env instead of process.env.

  9. Create CLAUDE.md at 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.
  10. 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)

Note

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.

  1. 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
  2. From the project root (taskflow-fetch-lab), start Claude Code and log in with your Per Scholas-provided Claude account when prompted:

    claude

    If you are not logged in, run /login inside the session.

  3. 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.
  4. 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.md as the source of truth. Do not write or edit any client code yourself.

  5. 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 dev

    Open http://localhost:5173 in your browser.

Note

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 --staged

Read 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).

#CheckWhat to look for
1Contract fidelityEvery function in client/src/api/tasks.js uses the exact method, path, and request body documented in docs/api-schema.md; nothing is invented.
2Error parsingThe wrapper throws on any non-2xx response and takes the message from the standard error object, not from a generic status text.
3ConfigurationThe base URL comes from import.meta.env.VITE_API_URL; no hardcoded http://localhost:5000 appears anywhere in client/src.
4Envelope unwrappingThe api functions return the data property of the success envelope, so components never dig through status themselves.
5Request statesLoading, error, and success states all render, and the error state shows the thrown message.
6HeadersA Content-Type: application/json header is sent on requests that have a body.
7BoundariesOnly files inside client/src were created or changed.

Step 3: Run the Functional Tests

With the backend and client both running, exercise every operation:

#TestHowExpected result
1Seed data loadsOpen http://localhost:5173A brief loading state, then the three seed tasks.
2CreateSubmit the form with a titleThe new task appears in the list.
3UpdateUse the mark-done button on a taskThe task’s status changes to done.
4DeleteUse the delete button on a taskThe task disappears from the list.
5Server validation surfacesSubmit the form with an empty titleThe server’s message “A non-empty title is required.” appears in the UI.
6Not-found errorOpen http://localhost:5000/api/v1/tasks/not-a-real-id in the browserThe raw standard error JSON with the code RESOURCE_NOT_FOUND.
7Network failureStop the server with Ctrl+C, then reload the clientThe 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.md
# 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 AbortController to the mount fetch in TaskList.jsx so 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)

  1. Create a new public repository on GitHub named taskflow-fetch-lab.

  2. 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.md with the Builder boundaries.
  • client/ with the AI-generated src/api/client.js, src/api/tasks.js, src/components/TaskList.jsx, the updated src/App.jsx, and .env.example.
  • CODE_REVIEW.md with 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:

CriteriaDescriptionPoints
Working Fetch LayerThe 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 AuditCODE_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 ProfessionalismThe repository contains all required artifacts with a clean structure and a commit history that separates setup, generation, and revision.5
Total30