Skip to content
bits
Your first Worker · Lesson 3 of 5 · 20 min

Hello, Worker

Desk: best at a computer, with a terminal

Verified node 24.22.2wrangler 4.157.0vitest 5.0.5typescript 5.9.3

Start from a-02 on GitHub. Starting here, or behind? Get the code at that tag:

Shell
git clone https://github.com/ryanyogan/klippy
cd klippy
git checkout a-02
Tutor:

Your Worker exists, but you haven't seen it run. In this lesson you'll run it on your laptop, teach it two answers, and write tests that prove both. The tests run inside the same runtime Cloudflare uses, so what passes here behaves the same once it's live.

Tutor:

1. Run it

From klippy/, start the local server:

Shell
pnpm dev

That runs wrangler dev in apps/agent (the root script from the last lesson). It prints Ready on http://localhost:8787. Open that address in your browser and you'll see "Hello World!". Leave this terminal running and open a second one for the commands below.

wrangler dev runs workerd, the open-source Workers runtime, on your machine. Save a change to your code and it reloads by itself.

If port 8787 is taken, run pnpm dev --port 8801 instead and use that port below. Our recordings use 8801.

Tutor:

2. A route and a 404

Right now every address answers "Hello World!". Klippy should answer at its home page, /, and say "Not found" for anything else. Replace apps/agent/src/index.ts with:

export default {
async fetch(request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response('Hi, I am Klippy.');
}
return new Response('Not found', { status: 404 });
},
} satisfies ExportedHandler<Env>;
TypeScriptUTF-8at a-03● lines 3–9: apps/agent/src/index.ts
Tutor:
  • new URL(request.url) splits the address into parts, so you can read its pathname: / for the home page, /nope for http://localhost:8787/nope.

  • A Response is a body plus a status. Leave the status out and it's 200 OK. 404 tells the caller, and any program reading the answer, that nothing lives there.

  • We dropped the env and ctx arguments c3 had. This code doesn't use them yet. env comes back in the next lesson.

Ask it with curl, a command that sends a request and prints the answer. -i shows the status line and headers too:

recorded output
curl -i http://localhost:8801/
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: text/plain;charset=UTF-8
Hi, I am Klippy.
recorded output
curl -i http://localhost:8801/nope
HTTP/1.1 404 Not Found
Content-Length: 9
Content-Type: text/plain;charset=UTF-8
Not found
Tutor:

The first terminal logs each request, like GET / 200 OK (20ms). If curl says "connection refused", the dev server isn't running, or it's on another port.

Tutor:

3. Types for what your Worker can reach

Your Worker will soon reach Cloudflare services: an AI model, a database. Each one arrives on env, and wrangler types reads wrangler.jsonc and writes their types into worker-configuration.d.ts. Run it now so you know the command:

Shell
pnpm --filter agent run cf-typegen
recorded output
pnpm --filter agent run cf-typegen
Generating project types...
interface __BaseEnv_Env {
}
✨ Types written to worker-configuration.d.ts
📣 Remember to rerun 'wrangler types' after you change your wrangler.jsonc file.
Tutor:

Env is empty: nothing is connected yet. You'll run this again each time you add something to wrangler.jsonc.

Tutor:

4. Two tests

A test is code that runs your code and checks the answer. Replace apps/agent/test/index.spec.ts with:

import { SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
describe('Klippy', () => {
it('says hi at /', async () => {
const response = await SELF.fetch('https://example.com/');
expect(response.status).toBe(200);
expect(await response.text()).toBe('Hi, I am Klippy.');
});
it('answers 404 for anything else', async () => {
const response = await SELF.fetch('https://example.com/nope');
expect(response.status).toBe(404);
});
});
TypeScriptUTF-8at a-03● lines 5–14: apps/agent/test/index.spec.ts
Tutor:
  • SELF is your Worker, running inside workerd for the test. SELF.fetch sends it a request, as curl did. The host, example.com, doesn't matter: the request never leaves your machine.

  • expect(...).toBe(...) fails the test if the two don't match.

  • Each it is one test with a sentence that says what should be true.

Tutor:

5. A typecheck, then run everything

TypeScript only checks your types when you ask it to. Add a typecheck script to apps/agent/package.json. It checks the Worker, then the tests, which have their own tsconfig.json:

"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"test": "vitest run",
"cf-typegen": "wrangler types",
"typecheck": "tsc --noEmit && tsc --noEmit -p test"
},
JSONUTF-8at a-03● lines 7–7: apps/agent/package.json
Tutor:

And one line to the root package.json, so pnpm typecheck checks every app:

"scripts": {
"dev": "pnpm --filter agent run dev",
"test": "pnpm --recursive run test",
"deploy": "pnpm --filter agent run deploy",
"typecheck": "pnpm --recursive run typecheck"
}
JSONUTF-8at a-03● lines 5–5: klippy/package.json
Tutor:

Now run both from klippy/:

Shell
pnpm typecheck
pnpm test
recorded output
pnpm test
> agent@0.0.0 test /home/ryan/code/klippy/apps/agent
> vitest run
✓ test/index.spec.ts (2 tests) 5ms
Test Files 1 passed (1)
Tests 2 passed (2)
Tutor:

pnpm typecheck prints nothing when all is well; an error names the file and line. If a test fails, vitest shows what it expected and what it got: a typo in "Hi, I am Klippy." is the usual one.

Save your work with git add -A and git commit -m "Hello, Worker".

quick check+20 neurons
Tutor:

Klippy's tests send requests with SELF.fetch. Why does it matter that they run inside workerd?

    Pick one option

    Pick one, then Check.