Introduction
This book is a learning roadmap tailored to one specific codebase: the FastVRP
route planning POC at /Users/leonlan/Dropbox/agents. It is written for a
backend engineer who needs to take that POC to a real product.
What you have today
- Backend: FastAPI + LangGraph ReAct agent (Claude Haiku 4.5, Gemini fallback), 12 tools that wrap a FastVRP solver client, in-memory session store.
- Frontend: React 19 + Vite, Leaflet map, SSE streaming chat, no state library (prop drilling), no TypeScript.
- Data: Scenarios held in a Python dict; instances are JSON files on disk.
- Deploy: Docker on a single GCE VM, nginx basic auth, Let's Encrypt cert.
The hard parts already work: the agent loop, the VRP schema, SSE streaming, and the solve-and-compare UX. The gap to "product" is almost entirely persistence, multi-user, and polish. It is not algorithm or agent work.
How to use this book
Each chapter covers one learning priority. Chapters are ordered by how much they unblock the rest of the work. Every chapter has the same shape:
- Why this matters in this codebase.
- What to learn, concretely.
- Resources worth reading.
- A hands-on exercise against the repo.
If you only have a weekend, read the Start here this week chapter and skip everything else.
TypeScript and React
Budget: 2-3 weeks.
Why this matters
The frontend is the weakest layer in the current codebase. It is plain JSX with no types. You have already said design quality matters. Adding types catches most of the bugs you will hit as you add features, and it gives future hires a codebase they can navigate without reading every function.
What to learn
| Topic | Time |
|---|---|
| Modern JavaScript: arrow functions, destructuring, spread, async/await, modules, array methods | 1-2 days |
| TypeScript handbook (first half): interfaces, types, generics, unions, narrowing | 2-3 days |
| React mental model: UI as a function of state, pure components, effects as sync boundaries | 2 days |
Hooks you will actually use: useState, useEffect, useMemo, useCallback, useRef | 2 days |
| One throwaway React app (todo list or weather fetcher) to get reps | 1 day |
openapi-typescript setup against the FastAPI /openapi.json | 0.5 day |
Port leaf components in frontend/src/components/ from .jsx to .tsx | 3-4 days |
Port App.jsx to App.tsx once the types are in place | 2 days |
Resources
- react.dev/learn: official React tutorial, the one to read cover to cover.
- TypeScript handbook: start with "The Basics," "Everyday Types," and "Narrowing."
- "A Complete Guide to useEffect" by Dan Abramov: the single best explanation of effects.
- "Before You memo()" by Dan Abramov: when not to reach for
useMemo/useCallback. - openapi-typescript: generates TS types from the FastAPI
/openapi.json.
Exercise
Port frontend/src/App.jsx and the components in frontend/src/components/
to TypeScript. Start with leaf components (MapSettingsPanel, RouteDetail,
KpiPanel) and work up to App.tsx. Run openapi-typescript against the
FastAPI server's /openapi.json and import the generated types into
useChat.ts so the SSE payloads are typed. When you are done, remove
allowJs from tsconfig.json and make sure the build is clean.
Notes
-
Read React quickstart and tutorial. I mostly learned to think more about state and setState, and props (which are data passed from parent to child component but not really state).
-
Went through https://www.typescriptlang.org/docs/handbook/2/basic-types.html. TypeScript is a superset of JavaScript designed in 2012 to catch type errors. Most notably, JavaScript has weird undefined behaviour when accessing properties that don't exist, which TypeScript will just catch. Type hinting works just like Python, but JavaScript is a nasty language with edge cases that I have yet to learn more about.
-
Went through https://www.typescriptlang.org/docs/handbook/2/everyday-types.html. Primitives are
string,number(no float/int separation), andboolean. Type-hinting really looks like Python style. What's different is object types, which are typed likefunction printCoord(pt: { x: number; y: number })to say thatpthasxandyproperties. Optional properties look likefunction printName(obj: { first: string; last?: string })wherelastmay be optional. This object notation is new to me, but fortunately there is type alias:type Point = { x: number; y: number; };or through interfaces:
interface Point { x: number; y: number; }The difference between
typeandinterfaceis thatinterfacecan be easily extended with new fields (but I don't know why you would do this). Always usestrictNullChecksto get rid of the weird null/undefined JavaScript behavior. -
JavaScript primers to read before going deeper into TypeScript:
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/A_re-introduction_to_JavaScript - the classic single-page primer. Covers types, operators, control flow, objects, arrays, functions, closures. Designed exactly for "I know how to program, just teach me JS." Start here.
- https://observablehq.com/@ballingt/javascript-for-python-programmers - very short Observable notebook, contrasts JS to Python directly. Good as a follow-up after MDN to nail the differences (truthiness,
==vs===,var/let/const, prototypes).
Developer tooling for TypeScript and React
Budget: 2-3 days.
Why this matters
Coming from Python you already have a stable mental model of the
toolchain: uv manages dependencies, ruff lints and formats, mypy or
pyright type-checks, pylsp powers the editor, and pyproject.toml
holds it all together. The JavaScript world has the same roles, but the
tools are split differently and the ecosystem moves faster. Knowing the
map up front means you spend time learning React, not fighting configs.
Python -> JS/TS map
| Role | Python | JavaScript / TypeScript |
|---|---|---|
| Runtime | CPython | Node.js (or Bun) |
| Package registry | PyPI | npm registry (registry.npmjs.org) |
| Package manager | uv, pip, poetry | npm, pnpm, bun |
| Lockfile | uv.lock, poetry.lock | package-lock.json, pnpm-lock.yaml, bun.lockb |
| Project config | pyproject.toml | package.json (+ tsconfig.json for TS) |
| Linter | ruff, pylint, flake8 | ESLint, Biome |
| Formatter | ruff format, black | Prettier, Biome |
| Type checker | mypy, pyright | tsc (the TypeScript compiler itself) |
| Editor LSP | pylsp, ruff-lsp, pyright | tsserver (built into VS Code) |
| Test runner | pytest | Vitest, Jest |
| Build tool / bundler | (none, Python is interpreted) | Vite, esbuild, Rollup, webpack |
What's actually different
- The compiler is the type checker.
tscplays the role ofmypy. It does not produce JavaScript at runtime in modern setups; Vite uses esbuild/swc for that. You runtsc --noEmitpurely to type-check, the same way you runmypy .. - There is no Python equivalent of bundlers. Browsers can't
importthousands of small files efficiently, so a build tool concatenates and tree-shakes your code. Vite is the current default. You already use it infrontend/. package.jsonispyproject.tomlplus a Makefile. Dependencies live underdependenciesanddevDependencies, and thescriptsblock is where you keepdev,build,lint,test. Run them withnpm run <name>.- Lint and format are still separate by default. ESLint lints, Prettier formats. Biome is the Rust-based newcomer that does both, and it's the closest thing to
ruffin spirit (single tool, fast, opinionated). For a new project today, Biome is reasonable; for an existing project with ESLint already wired up (like yours), staying on ESLint + adding Prettier is the path of least resistance. - Package manager choice matters less than in Python.
npmis fine.pnpmis faster and uses a content-addressable store (closest touv's vibe).bunis the all-in-one upstart. Pick one and stick with it; yourfrontend/already usesnpmbased onpackage-lock.json. tsconfig.jsonis where TypeScript's strictness lives. Turn onstrict: truefrom day one. That single flag enablesstrictNullChecksand friends, which is where most of TypeScript's value comes from.
What to learn
| Topic | Time |
|---|---|
package.json anatomy: dependencies, devDependencies, scripts, the type: module field | 0.5 day |
npm commands you'll actually use: install, run, ci, update, outdated | 0.5 day |
tsconfig.json: strict, target, module, moduleResolution, paths | 0.5 day |
ESLint v9 flat config: how eslint.config.js works, how plugins compose | 0.5 day |
| Prettier (or Biome) setup and editor-on-save formatting | 0.5 day |
Vite: dev server, vite build, plugins, env vars (import.meta.env) | 0.5 day |
| Vitest: writing a first test, running in watch mode | 0.5 day |
| VS Code TypeScript integration: go-to-def, rename symbol, organize imports | 0.5 day |
Resources
- Node.js docs: skim the "Learn" section, especially modules and
package.json. - npm docs: package.json reference: the canonical reference for fields and scripts.
- TypeScript tsconfig reference: every option, with examples. Use as a lookup, not a read-through.
- ESLint flat config docs: v9 ditched the old
.eslintrcformat. Read this before touchingeslint.config.js. - Prettier docs: tiny surface area on purpose. Read the "Options" page once.
- Biome docs: if you want to try the all-in-one alternative.
- Vite guide: start with "Why Vite" and "Features."
- Vitest docs: Jest-compatible API, but Vite-native and fast.
Starter project
Before you touch the FastVRP frontend, scaffold a small throwaway app from scratch. The goal is to feel each tool's role on its own, with no legacy config in the way.
Check your Node and npm
node -v # v20 or newer
npm -v # v10 or newer
If either is missing or old, install Node from nodejs.org
or via nvm. npm ships with Node, so you only install one thing.
Scaffold with the shadcn Vite starter
One command gives you React + TypeScript + Vite + Tailwind v4 + shadcn, with path aliases already wired:
npx shadcn@latest init -t vite
npm install
That saves you from manually configuring Tailwind, the @ alias in both
tsconfig.json and vite.config.ts, and the shadcn init. You could do
each step by hand (and it's worth reading the shadcn Vite guide
once to see what the starter is doing), but for a throwaway app the
starter is the cleanest path.
Add a component or two when you need them:
npx shadcn@latest add button input checkbox
The components land in src/components/ui/. Read them. They are short,
typed, and built on Radix primitives. This is the easiest way to see what
"good" React + TS + Tailwind code looks like.
Add Biome
The shadcn starter does not include a linter or formatter, so layer Biome on top:
npm install --save-dev --save-exact @biomejs/biome
npx biome init
Then add scripts to package.json:
"scripts": {
"format": "biome format --write .",
"lint": "biome lint .",
"check": "biome check --write ."
}
check runs lint and format together and fixes what it safely can. Install
the Biome VS Code extension and turn on "format on save" so you stop
thinking about formatting.
Check the .gitignore
The shadcn Vite starter ships with a .gitignore. Make sure it covers at
least:
node_modules
dist
.env
.env.local
.DS_Store
node_modules and dist are the big two: never commit your dependency
tree or build output. .env* keeps secrets out of git. .DS_Store keeps
macOS Finder droppings out of your commits.
Run Biome in CI
Add .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
biome:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx biome ci .
biome ci is the read-only flavor: it lints, format-checks, and
import-sorts, and fails the job if anything is off. It does not write
files. That's the right behavior for CI: a PR that drifts from your rules
should fail, not silently auto-fix.
If you want type-checking on every push, add a second step:
- run: npx tsc --noEmit
What to build
A todo list with three features:
- Add, remove, and toggle items, using shadcn's
Button,Input, andCheckboxfor the UI. - Persist to
localStorageso a refresh keeps the list. - One typed shape,
type Todo = { id: string; text: string; done: boolean }, threaded through state, props, and the storage helper.
Style only with Tailwind utility classes. No router, no state library, no
backend. The point is to use useState, useEffect, typed props, and
utility-first styling on something that runs end to end.
What you'll learn
- How
tsc --noEmitcatches a typo that would have crashed at runtime. - What Biome flags out of the box, and which rules you actually want.
- How
npm run devandnpm run builddiffer in what they produce. - How Tailwind classes compose, and when to extract a component instead of duplicating a long class string.
- How shadcn components are wired: Radix primitives,
cn()for class merging, variants viaclass-variance-authority.
Throw it away when you're done. The reps are the point.
Exercise
Audit frontend/ and write down what role each tool currently plays:
package.json: dependencies, dev dependencies, scripts.package-lock.json: lockfile.eslint.config.js: lint rules.vite.config.js: build and dev server config.- (Missing)
tsconfig.json: add this when you start chapter 1's TypeScript port. - (Missing)
prettierconfig or Biome: add one before the TS port so formatting stays consistent. - (Missing) Vitest: add when you write the first test.
Then run each script once (npm run dev, npm run lint, npm run build) and read the output. The point is to know what each tool actually does, not to change anything yet.
Notes
TanStack Query and frontend state
Budget: 1 week.
Why this matters
Today App.jsx manually fetches sessions and scenarios with useEffect and
local useState. The second the product has features like "save scenario,
list my scenarios, compare across runs," ad-hoc fetch calls fall apart.
You end up reimplementing caching, loading flags, refetch-on-focus, and
optimistic updates by hand. TanStack Query does all of that and is the
default choice for new React apps.
What to learn
| Topic | Time |
|---|---|
useQuery for reads, with a typed fetch client | 1 day |
| Query keys: how to design them so invalidation is surgical | 0.5 day |
useMutation for writes, and when to invalidateQueries vs return new data | 1 day |
| Optimistic updates for mutations that should feel instant | 0.5 day |
Defaults: stale time, retry, refetchOnWindowFocus, and when to override | 0.5 day |
| Devtools: installing and reading the TanStack Query devtools panel | 0.5 day |
Port every useEffect-based fetch in App.jsx to useQuery or useMutation | 2-3 days |
Resources
- TanStack Query docs: the official reference.
- Practical React Query by TkDodo: the best long-form treatment, read as a series.
- TanStack Query devtools: install and keep open while porting.
Exercise
Replace every useEffect(() => fetch(...)) in App.jsx and the hooks in
frontend/src/ with useQuery. Replace every POST or PATCH call with
useMutation. When a scenario is created or renamed, invalidate the
scenario list query instead of manually reloading. Keep the SSE chat stream
as is. TanStack Query is for request/response, not for streams.
Notes
A real map library
Budget: 1 week.
Why this matters
MapPanel.jsx uses Leaflet. Leaflet is fine for a few markers and
polylines, but it struggles once you have thousands of stops or want custom
styling beyond DOM elements. MapLibre GL JS renders vector tiles on the
GPU, handles large GeoJSON sources cleanly, and gives you data-driven
styling. For a route planning tool, this is the piece that most affects
whether the product feels real or feels like a demo.
What to learn
| Topic | Time |
|---|---|
| The MapLibre concept stack: style, sources, layers | 1 day |
GeoJSON sources and circle, line, symbol layers | 1 day |
| Data-driven styling with expressions (color by vehicle, width by load) | 1 day |
| Clustering for dense point sets | 0.5 day |
| Fit-to-bounds and camera transitions for scenario switching | 0.5 day |
react-map-gl as a thin React wrapper (optional but helpful) | 0.5 day |
Rewrite MapPanel.jsx on top of MapLibre with GeoJSON sources | 2-3 days |
deck.gl overlay basics (only if you actually need heavy viz) | defer |
Resources
- MapLibre GL JS docs: concepts, style spec, API reference.
- MapLibre style spec: the reference you will keep open while styling layers.
- MapLibre examples: copy-paste starting points for clustering, expressions, feature-state.
- react-map-gl: thin React wrapper, optional but helpful.
- deck.gl: GPU-accelerated overlays, for later.
- OpenFreeMap: free basemap tiles if you need to stay zero-cost.
Exercise
Rewrite frontend/src/components/MapPanel.jsx on top of MapLibre GL JS.
Represent tasks and depots as GeoJSON sources rather than per-marker React
components. Draw selected routes as a line layer driven by a
feature-state so highlighting a route does not rerender the whole map.
Wire the existing polyline decoding in server.py into the GeoJSON
features returned by the API.
Notes
Persistence and auth
Budget: 1-2 weeks.
Why this matters
SessionStore in models.py is an in-memory Python dict. Restart the
server and everything a user built is gone. There are no user accounts, so
every visitor shares the same nginx basic-auth credential. This is the
single largest gap between the current POC and anything you can charge
for.
This is also the chapter where your backend instincts carry you the furthest. You already know SQL, you already know how to model domains. What is new is the Python async plus SQLAlchemy 2.0 patterns, and wiring authentication into FastAPI.
What to learn
| Topic | Time |
|---|---|
| Postgres refresher: transactions, indexes, JSONB for VRP payloads | 1 day |
| SQLAlchemy 2.0 async sessions, or SQLModel if you prefer Pydantic | 2-3 days |
| Alembic migrations: autogenerate, edit, apply, roll back | 1 day |
| Pick and set up a managed auth provider (Clerk, Supabase Auth, WorkOS) | 1-2 days |
| JWT verification as a FastAPI dependency | 0.5 day |
Repository layer that scopes every query by user_id | 1-2 days |
| Design the schema (users, sessions, scenarios, solutions) | 0.5 day |
Replace SessionStore and write the isolation integration test | 2-3 days |
Resources
- SQLAlchemy 2.0 docs: start with the "Unified Tutorial."
- SQLAlchemy async ORM: the async session patterns you will use with FastAPI.
- SQLModel: Pydantic-flavored alternative if you prefer it.
- Alembic tutorial: migrations in half a day.
- FastAPI dependencies: where your JWT check will live.
- FastAPI SQL databases: official async SQL walkthrough.
- Managed auth providers: Clerk, Supabase Auth, WorkOS.
Exercise
Design and implement the schema for users, sessions, scenarios, and
solutions. Keep the VRP request and solution payloads as JSONB. Replace
SessionStore with a repository that reads and writes Postgres. Add a
FastAPI dependency that resolves a JWT to a user_id and injects it into
every request. Write one integration test that proves a user cannot read
another user's scenario even if they guess the ID.
Schema sketch
create table users (
id uuid primary key,
email text unique not null,
created_at timestamptz not null default now()
);
create table sessions (
id uuid primary key,
user_id uuid not null references users(id),
name text not null,
created_at timestamptz not null default now()
);
create table scenarios (
id uuid primary key,
session_id uuid not null references sessions(id),
name text not null,
request jsonb not null,
original_request jsonb not null,
created_at timestamptz not null default now()
);
create table solutions (
id uuid primary key,
scenario_id uuid not null references scenarios(id),
status text not null,
payload jsonb,
created_at timestamptz not null default now()
);
create index on sessions (user_id);
create index on scenarios (session_id);
create index on solutions (scenario_id);
Notes
Background jobs
Budget: 3-5 days.
Why this matters
Solves can take minutes. Today solve_vrp blocks the request. That is fine
for a demo with one user, but it holds a worker the whole time, it breaks
on server restarts, and it has no retry path when FastVRP transiently
fails. A queue fixes all three.
What to learn
| Topic | Time |
|---|---|
| Queue concepts: producers, consumers, visibility timeouts, dead letters | 0.5 day |
| Pick one of Celery, RQ, or arq and read its quickstart (arq fits this async codebase best) | 0.5 day |
| Redis as the broker: install, run, connect | 0.5 day |
| Progress reporting pattern: worker writes to Postgres, SSE handler reads | 1 day |
Move solve_vrp from tools.py into an arq job | 1-2 days |
| Retry policy with exponential backoff for FastVRP timeouts | 0.5 day |
Resources
- arq docs: async-native task queue, closest fit to this codebase.
- Celery docs: heavier, battle-tested alternative.
- RQ docs: simple sync worker alternative.
- FastAPI background tasks: read this to understand why they are not a substitute for a real queue.
- Redis docs: you only need the basics to use it as a broker.
Exercise
Move the body of solve_vrp in tools.py into an arq job. The tool
invocation should enqueue the job and return a job ID. Store status and
result on the solutions table added in the previous chapter. Update
the SSE chat handler so when the agent is waiting on a solve, it streams
progress updates as they land in Postgres. Add a retry policy that retries
FastVRP timeouts up to three times with exponential backoff.
Notes
LLM agent engineering
Budget: ongoing.
Why this matters
You already use LangGraph in agent.py. The loop works. What is missing
is the set of production concerns that separate a demo agent from one
that is cheap, debuggable, and stable under prompt edits.
What to learn
| Topic | Time |
|---|---|
Prompt caching: add cache_control to the system prompt in agent.py | 2 hours |
| Measure token cost before and after caching on a representative session | 1 hour |
| Streaming structured output so the UI can render partial tool progress | 1 day |
Write 10 scripted eval cases at tests/agent_evals.jsonl | 1 day |
| Build a pytest-based eval harness that runs the agent against each case | 1-2 days |
| Wire LangSmith tracing behind an env flag | 0.5 day |
| Ongoing: review traces weekly, add eval cases when you find regressions | ongoing |
Resources
- Anthropic prompt caching: the official guide, the only thing you need for step 1 of the exercise.
- Anthropic tool use: if you are reworking the tool layer.
- LangGraph docs: concepts and recipes.
- LangSmith: hosted tracing and eval platform.
- promptfoo: lighter-weight eval framework.
- The
claude-apiskill in this environment. Invoke it when you touchagent.pyorsystem_prompt.md. It enforces caching and model-version hygiene.
Exercise
- Turn on prompt caching for the system prompt in
agent.py. Measure token cost on a representative session before and after. - Add a JSONL eval file at
tests/agent_evals.jsonlwith 10 scripted user turns and the expected tool calls or substrings in the final response. Write a pytest that runs the agent against each entry and fails if the expected tool was not called. - Wire LangSmith tracing behind an env flag so you can turn it on in staging without leaking traces from production by default.
Notes
Design fundamentals
Budget: weekends, parallel to everything.
Why this matters
Your product is a spatial data visualization with a chat sidebar. Design quality is most of what users will feel. You do not need to become a designer. You need enough taste and vocabulary to recognize when something is wrong and fix it.
What to learn
| Topic | Time |
|---|---|
| Visual hierarchy, spacing, and color (most "looks off" bugs are these three) | 1 weekend |
| Typography: font choice, weight scale, line-height, line length | 1 afternoon |
| Cartographic principles: color vs size vs shape, avoiding chartjunk | 1 weekend |
| The CRAP acronym: contrast, repetition, alignment, proximity | 2 hours |
| Refactoring UI (Wathan and Schoger), cover to cover | 1 weekend |
| Practical Typography (Butterick), online | 1 afternoon, reread yearly |
| The Visual Display of Quantitative Information (Tufte) | 1-2 weekends |
| The Design of Everyday Things (Norman) | 2-3 weekends |
Apply it: one-hour design pass on MapSettingsPanel.jsx and RouteDetail.jsx | 2 hours |
Resources
- Refactoring UI by Adam Wathan and Steve Schoger. Written for developers, not designers. Most tactical book on the list.
- The Visual Display of Quantitative Information by Edward Tufte. Spatial data is Tufte's home turf.
- Practical Typography by Matthew Butterick. Free online. Read once a year.
- The Design of Everyday Things by Don Norman. Not about web design, but about how humans form mental models of systems. Everything else on this list is downstream of it.
- shadcn/ui: accessible primitives you own and can modify, paired with Tailwind.
- Tailwind CSS docs: the layout and spacing sections are where the design work actually happens.
Exercise
Read Refactoring UI cover to cover in one weekend. Then do a one-hour
pass on MapSettingsPanel.jsx and RouteDetail.jsx applying what you
learned. Fix spacing, align controls on a grid, reduce the number of
distinct shades of gray, and pick one font weight scale and stick to it.
Do not refactor the logic. Only touch styles. Commit before and after so
you can see the delta.
Notes
Observability and deploy
Budget: 1 week, once users exist.
Why this matters
Your current deploy is one VM plus nginx. That is fine today. The day a user reports "my solve failed and I don't know why," you will wish you had structured logs, error tracking, and a CI pipeline that builds known good images. Do this before users arrive, not after.
What to learn
| Topic | Time |
|---|---|
structlog in Python: JSON in prod, pretty in dev | 0.5-1 day |
| Pick and wire a managed log destination (Axiom, Better Stack, Grafana Cloud) | 0.5 day |
| Sentry for the FastAPI backend | 0.5 day |
| Sentry for the React frontend, sourcemaps included | 0.5 day |
| GitHub Actions: one workflow for pytest + frontend build + Docker image | 1-2 days |
| Push Docker images to Artifact Registry on merge to main | 0.5 day |
| Evaluate Cloud Run or Fly.io as the next deploy target (defer the migration) | 0.5 day |
Resources
- structlog docs: structured logging for Python.
- Sentry for FastAPI: backend error tracking.
- Sentry for React: frontend error tracking with sourcemaps.
- GitHub Actions docs: the workflow syntax reference.
- Cloud Run docs: the next deploy target when a single VM is not enough.
- Fly.io docs: alternative to Cloud Run, slightly simpler DX.
- Managed log destinations: Axiom, Better Stack, Grafana Cloud.
Exercise
Replace server.log and ad-hoc print statements in server.py with
structlog. Configure it to emit JSON in production and pretty
human-readable output in development. Add Sentry to both the backend
and the frontend. Write one GitHub Actions workflow that runs tests,
builds the Docker image, and pushes it to Artifact Registry on a push
to main. Leave the VM deploy as is for now. The goal is to know when
something is broken, not to rearchitect the deploy.
Notes
What to skip
Just as important as what to learn is what to ignore. A backend engineer new to frontend will get pulled in every direction by blog posts and hype. These are the things that will come up, and that you should not touch for this product, at this stage.
Next.js, server components, SSR
You are building an authenticated internal tool and an authenticated product for paying users. You do not need SEO on the solver, and you do not need SSR. A plain Vite SPA talking to FastAPI is simpler, ships faster, and is easy to migrate later if marketing needs demand it.
Redux, Zustand, and other global state managers
TanStack Query covers 90% of what
you would reach for them for. The remaining 10% is usually a few
useStates in App.tsx. If you find yourself genuinely needing a
global store for cross-cutting UI state, reach for
Zustand at that point. Not before.
Component libraries beyond shadcn/ui plus Tailwind
shadcn/ui gives you accessible primitives you own and can modify. Do not install Material UI, Chakra, Ant Design, Mantine, or any other framework on top. They fight Tailwind and they fight your designer instincts.
Microservices
One FastAPI app plus one worker process is the right shape for a long time. You do not need a service mesh, you do not need gRPC between your own services, and you do not need to split the agent into its own service. Split only when a single service is genuinely in the way.
Kubernetes
Cloud Run or Fly.io will serve you well past your first hundred paying users. Kubernetes is worth learning the day you have a dedicated SRE who wants to run it. Not before.
Testing frameworks and Storybook, too early
Both are good tools. Both are premature if the UI is still changing every day. Add Vitest the day you have a regression you want to catch. Add Storybook the day you have a component library someone else needs to use.
Start here this week
If you only have a weekend, do these three things. They are the highest leverage per hour in the whole book.
1. Turn on Anthropic prompt caching
Effort: two hours. Impact: immediate and ongoing cost reduction, plus latency.
In agent.py, add cache_control to the system prompt so it hits
Anthropic's prompt cache.
system_prompt.md is 101 lines and ships on every turn. Caching it cuts
token cost by a large factor. Invoke the claude-api skill when you do
this, so the diff also handles model version hygiene.
2. Add TypeScript to the frontend
Effort: one weekend. Impact: catches most of the bugs you will hit in the next three months.
Add a tsconfig.json, rename the leaf components in
frontend/src/components/ from .jsx to .tsx, and run
openapi-typescript against the FastAPI
server's /openapi.json to generate a typed client. Import those types
into useChat.js and App.jsx as they get ported. You do not have to
port everything in one pass. Leaving a few files as .jsx is fine.
3. Read Refactoring UI and do one design pass
Effort: one weekend. Impact: biggest visible-quality bump for the effort.
Read Refactoring UI cover to cover.
Then spend one focused hour on MapSettingsPanel.jsx and
RouteDetail.jsx applying what you just learned. Fix spacing, align on
a grid, cut the number of gray shades, and pick a type scale. Only
touch styles, do not refactor logic.
What comes next
Once these three are done, open TypeScript and React and work through the chapters in order. Persistence and auth is the chapter that unblocks paying customers. Everything before it is quality-of-life. Everything after it is scale and polish.