Project overview · local teaching tool

Anthropic Chat Lab

A chat app for learning the Claude API by watching it work — every request, every response, and what each one cost.

Source
15,763lines of TypeScript
Patterns
7switchable
Tools
76 client, 1 server
Built from
68steps, 10 phases

What it is

The premise

A normal chat window with an activity log beside it. The log shows every request that leaves the machine and every response that comes back — the actual JSON, not a summary — along with what each one cost and why it happened.

You can switch the orchestration pattern, the models, the tools, the system prompt, caching, thinking and effort between messages, and watch the numbers change. It is built for people preparing for the Anthropic certifications, and for anyone who would rather see stop_reason: "tool_use" arrive than read about it.

It runs entirely on one machine. No database, no Docker, no hosting. The only network calls go to the Anthropic API.

The measurement that makes the point

Asked what stop_reason: end_turn means, the Terse system prompt answered in 23 output tokens. Teaching assistant took 589 — 25× the cost for the same fact. The system prompt is the cheapest lever in the app and the easiest to overlook.

Tech stack

Node · Express · vanilla TypeScript · no frontend framework

Runtime dependencies

PackageVersionRole
@anthropic-ai/sdk1.8.0The Claude API client. One shared instance, with baseURL pinned to api.anthropic.com
express5.3.0HTTP server and static file host — 17 routes
zod4.6.7Schemas for structured outputs via output_config.format
@modelcontextprotocol/client2.4.0Connects to an MCP server, so its tools join the Tools tab beside the local ones
dotenv18.0.10Loads an optional .env key at startup

Build and dev toolchain

PackageVersionRole
typescript7.0.2Two configs — one per half of the app
tsx4.23.15Runs TypeScript directly, restarts on save. The normal way to use the app
@types/node26.6.6Server-side type definitions
@types/express5.0.6Route and middleware types

Platform

Node.js20 or newer required; developed on 24.4.1
Module system"type": "module" — ESM throughout, which means .js extensions on imports of .ts files
FrontendHand-written TypeScript compiled to ES modules. No React, no bundler, no CSS framework
Default modelclaude-opus-5-5, with claude-haiku-5-5 as the small model

The two TypeScript configs

The split is a security boundary, not a build convenience. The server config targets ES2022 with module: NodeNext, strict: true, and types: ["node"]. The client config sets "types": [] — deliberately empty — so server-only globals like process cannot be referenced from browser code even by accident. The API key is never reachable from the frontend half.

Why no framework

The page is the teaching surface. A framework would put its own abstraction between the reader and the JSON on the wire, which is the one thing the app exists to show. The Markdown renderer is hand-written for a related reason: model output renders through textContent, never innerHTML, and a packaged renderer would not guarantee that.

What's in it

Patterns, tools, controls

Seven orchestration patterns

PatternRequestsWhen it earns its cost
Single call1The baseline every other pattern has to beat. Does not run tools
Tool loop2–4Anything the model cannot know: the time, a calculation, a file
Prompt chaining2Long structured answers — a plan call, then a writing call
Routing2Mixed question types one system prompt cannot serve
Parallelisation4Several angles that all matter. Buys latency, not money
Orchestrator + workers3–7Separable parts, where what the parts are depends on the question
Evaluator + optimiser2–6When being wrong is expensive and being slow is not

Seven tools

ToolRuns onWhat it teaches
calculatorclientThe basic loop. Arithmetic the model would get wrong
get_current_timeclientSomething the model genuinely cannot know
notesclientState across calls, and multi-step loops
search_docsclientRetrieval over a local corpus
read_fileclientSandboxing — resolve the path first, then check it is still inside
flaky_serviceclientControllable failure. failRate: 1 fails every time
web_searchserverRuns on Anthropic's infrastructure. No loop, no client code

Controls

System promptsFive — terse, teaching, exam, beginner, none
Models by roleDeciding, producing, judging — assigned separately, because a classifier does not need the big model
Prompt cachingoff, prefix, full
EffortFive levels via output_config.effort, which replaced budget_tokens
Extended thinkingDisplay only — thinking is always on and always billed on these models
Tool choiceauto, none, and two forced options current models reject with a 400, left in so the error is visible

How it was built

docs/build-plan.html · 68 steps · 10 phases

The project was built from a self-contained build plan that sits in the folder it describes. It is one HTML file with its own styling, dark mode, a progress counter, and checkboxes that persist to browser storage. Each step is sized for under 30 minutes and ends with a done-check you can run.

  1. Project skeleton 01–06
  2. First Claude call 07–10
  3. Server and static frontend 11–16
  4. The log channel 17–22
  5. Real chat, streaming 23–28
  6. Example tools 29–37
  7. The tool-use loop 38–45
  8. Orchestration patterns 46–55
  9. Certification topics 56–62
  10. Teaching polish 63–68

Each step has three parts

What to do, a done check — a literal command plus its expected output, such as npx tsx scripts/check-env.ts printing PASS — sk-ant-a… — and often a gotcha recording a trap hit in practice. The plan tells you that if a step stalls, paste it back to Claude and ask for it to be split into 23a, 23b.

Gotchas worth keeping

Conventions held through the build

What the code enforces on itself

Every file explains itself

Each file opens with a comment saying what it does and why it exists; npm run audit-comments checks none were missed. These record design history rather than restating the code. The tool registry's header explains that an earlier version put the interface in the registry file, creating an import cycle that worked with one tool and threw Cannot access 'currentTime' before initialization with two.

Dependency order as a rule

Tools follow three levels: types.ts imports nothing, definitions/ imports types, registry.ts imports the definitions. A module defining shared types belongs at the bottom of the graph. The frontend holds a matching rule in the other direction — app.ts imports from log.ts, settings.ts and transcript.ts, and nothing imports app.ts.

Security by construction

Current state

Verified 11 November 2026

One caveat

The pricing rates are hard-coded in server/usage.ts and will go out of date. The cost figures are for noticing that one pattern costs four times another, not for billing.