The 60-Minute Chatbot · Lesson 3 of 5
Lesson 3 — The Wiring
> 8 minutes · the Express server, the API call, the chat UI
The hard thinking is done. The system prompt and facts block are written. Now we just need 50 lines of plumbing to connect a browser form to Claude.
If you're new to backend code, read this slowly. The whole point of this course is that you can read every line and understand it. There are no abstractions hiding things from you.
---
Open `server/index.js` and scroll past the prompt to the Express setup.
```js require('dotenv').config() const express = require('express') const path = require('path') const Anthropic = require('@anthropic-ai/sdk')
const app = express() const port = process.env.PORT || 3000 const ai = new Anthropic() // reads ANTHROPIC_API_KEY from .env ```
Piece 1 — what we just imported
- `dotenv` loads your `.env` file so `process.env.ANTHROPIC_API_KEY` works
- `express` is the web server (handles HTTP requests, serves files, parses JSON)
- `path` is built into Node — used to find the static files folder
- `@anthropic-ai/sdk` is Anthropic's official Claude library — one line to call the API
When we say `new Anthropic()` with no arguments, the library automatically reads `ANTHROPIC_API_KEY` from your environment. If your key isn't there or is wrong, the first API call will fail with a 401.
---
```js app.use(express.json()) app.use(express.static(path.join(__dirname, '..', 'public'))) ```
Piece 2 — middleware
Two lines do a lot:
- `express.json()` auto-parses incoming JSON bodies so we can read `req.body.question` directly
- `express.static(...)` serves any file in `public/` at the matching URL — so `public/index.html` is reachable at `http://localhost:3000/`
That's it. No router gymnastics. Two middlewares.
---
```js app.post('/chat', async (req, res) => { const question = (req.body?.question || '').trim() if (!question) return res.status(400).json({ error: 'Please type a question.' })
try { const response = await ai.messages.create({ model: 'claude-haiku-4-5-20251001', max_tokens: 400, system: SYSTEM_PROMPT, messages: [{ role: 'user', content: question }], }) const answer = response.content .filter(c => c.type === 'text') .map(c => c.text).join('').trim()
res.json({ answer }) } catch (err) { console.error('Anthropic error:', err.message) res.status(500).json({ error: 'Sorry — something went wrong. Try again?' }) } }) ```
Piece 3 — the actual API call
This is the heart of the whole app. Let's read it line by line.
- `app.post('/chat', ...)` — when the browser POSTs to `/chat`, run this function
- `req.body.question` — the user's typed message
- `ai.messages.create({...})` — the Anthropic call
- `model: 'claude-haiku-4-5-20251001'` — Haiku is the small/fast/cheap model. Perfect for this. Cents per thousand requests. Latency under a second.
- `max_tokens: 400` — cap the answer so it can't ramble. About a paragraph and a half.
- `system: SYSTEM_PROMPT` — the receptionist instructions from Lesson 2
- `messages: [{ role: 'user', content: question }]` — the actual user message
- `response.content.filter(...).map(...)` — Claude returns content in blocks. We grab the text blocks and join them. (Other block types come into play with tools and images — not this course.)
- `res.json({ answer })` — send the answer back to the browser
That's the whole AI integration. Six meaningful lines.
> Why isn't this streaming? Because for a chatbot answering one question at a time, the round-trip latency is already under a second on Haiku. Streaming adds complexity. We'll cover it in the Playbook.
---
Open `public/index.html`. It's a single file. No framework, no build, no transpiler. Look at how short it is.
The page renders a chat card, a message list, and a form. When you submit:
```js const res = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question }), }) const data = await res.json() // then puts data.answer into the chat ```
That's it. The browser POSTs the question, the server hits Claude, the answer comes back, the browser appends it to the chat. No abstractions hiding anything.
---
In your terminal, from the project folder:
```bash npm start ```
You should see:
``` ✅ Comfort Zone chatbot is running. Open http://localhost:3000 in your browser. ```
Open the URL. You should see the chat card. Try one of these:
- "How much is a tune-up?"
- "What time do you close on Saturday?"
- "Do you service Trane systems?"
- "I have no heat and it's 8pm — what do I do?"
- "Do you do duct cleaning?"
Notice the last two in particular. The "no heat" question should route you to the 24/7 emergency line. The "duct cleaning" question should politely say no, because we explicitly listed it under "We do NOT" in the facts block.
If something doesn't work, jump back to your terminal and look at the error. The most common ones:
- `401 Unauthorized` — your API key in `.env` is wrong or empty
- `500 Internal Server Error` — usually means the API call itself failed. Check terminal output.
- `Cannot GET /` — you're hitting the wrong URL. Use `http://localhost:3000`, not the IP.
---
A working chatbot. Today.
You can already screenshot this and put it on LinkedIn. You can record a 30-second Loom of it answering questions. You can show it in an interview.
But it's still pretty bare. Move to Lesson 4 — we'll spend 6 minutes making it look and sound like a real Comfort Zone product.
Previous: Lesson 2 — The Brain · Next: Lesson 4 — The Branding