Your Builder Setup: Node, TypeScript, and API Keys
Set up Node.js 22+, a TypeScript project, provider API keys stored safely, and a scratch playground you'll use to try every idea in the course.
The first hour with a new tool decides how the next hundred go. Spend it fighting module errors, a key that won't load, or a script you have to rewrite for every experiment, and each new idea starts with friction. Spend it building a clean setup, and trying an idea becomes a one-line command you'll run without thinking.
In this lesson you'll set up everything the code track needs: Node.js 22 or newer, a strict TypeScript project, API keys stored where they can't leak and validated at startup, a shared AI module that every script uses, and a reusable playground: a small command-line runner that sends any prompt and input file to any model tier and reports the output, tokens, and timing. You'll use it in almost every lesson that follows.
What you're setting up and why
Here's the whole kit. Each piece earns its place by preventing a specific kind of pain later.
| Piece | What it is | Why you want it |
|---|---|---|
| Node.js 22+ | The JavaScript runtime | Modern ES modules, top-level await, built-in .env loading |
| TypeScript (strict) | Types for JavaScript | Catches the classic bug of reading .text on a block that isn't text |
| tsx | Runs .ts files directly | No build step between editing and trying |
@anthropic-ai/sdk | Anthropic's official TypeScript SDK | Typed requests and responses, retries, streaming |
| Zod | A schema library | Validates config now; validates model output from Module 4 on |
.env + validation | Keys outside your code, checked at startup | No leaked keys, no mysterious 401 errors mid-experiment |
| The playground | src/play/try.ts | Try any prompt on any input in seconds |
If you've already got a Node and TypeScript project you like, skim this lesson for the key-handling and playground sections; those are the parts specific to AI work.
Install Node.js 22 or newer
Check what you have:
node --versionIf it prints v22 or higher, you're set. If not, install the current long-term support (LTS) release. The cleanest way is a version manager, a tool that installs Node versions side by side and switches between them per project, such as fnm or nvm on macOS and Linux, or fnm or nvm-windows on Windows. The installer from nodejs.org works too if you only need one version.
Node 22 is the floor for this course. Node 20 reached end of life in April 2026, and the examples use features (like the --env-file flag and stable ES module behavior) that older versions handle poorly or not at all.
Create the playground project
Make a folder and install the packages:
mkdir ai-playground && cd ai-playground
npm init -y
npm pkg set type=module
npm install @anthropic-ai/sdk zod
npm install --save-dev typescript tsx @types/node
mkdir -p src/lib src/play prompts inputsnpm pkg set type=module tells Node to treat your .js and .ts files as ES modules, which you need for import syntax and top-level await.
Add a tsconfig.json:
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["src"]
}NodeNext makes TypeScript follow Node's real module rules, which is why imports between your own files end in .js even though the files are .ts (tsx and TypeScript both map them correctly). strict is non-negotiable for AI code: model responses are unions of block types, and strict mode forces you to check which one you have.
Finally, add scripts to package.json so you never type long commands:
{
"scripts": {
"hello": "tsx --env-file=.env src/play/hello.ts",
"try": "tsx --env-file=.env src/play/try.ts",
"check": "tsc --noEmit"
}
}The --env-file=.env flag is Node's built-in way to load environment variables from a file; tsx passes it through. No extra library needed.
API keys: get them, store them, limit them
An API key is a secret string that identifies your account to a provider and lets requests bill to it. Treat it like a password with a credit card attached.
Get an Anthropic key. Sign in to the Claude Console at platform.claude.com, add credits or a payment method, and create a key. Copy it right away; you can't view it again later.
Set a spend limit before you write any code. The Console lets you cap your organization's monthly API spend (look for the limits or billing settings). A loop bug that calls the API ten thousand times then becomes an error message instead of an invoice. You can also use workspaces to keep separate keys and limits for experiments and production.
Store it in .env:
ANTHROPIC_API_KEY=sk-ant-your-key-here
# Optional: only needed for lessons that compare providers or need embeddings
# OPENAI_API_KEY=sk-your-openai-keyKeep it out of git, and commit a template instead so others (and future you) know which variables exist:
node_modules
.env
.env.*
!.env.exampleANTHROPIC_API_KEY=
# OPENAI_API_KEY=You'll want an OpenAI or Google key later for lessons that compare providers or need capabilities Anthropic doesn't offer, such as embeddings (Module 5) and speech and image generation (Module 7). There's no need to create them today.
Validate configuration at startup
The most common setup bug isn't a missing key; it's a missing key that you only discover three calls into an experiment, through a confusing authentication error. Fix that once by validating the environment the moment your code starts:
import { z } from "zod";
const EnvSchema = z.object({
ANTHROPIC_API_KEY: z.string().min(1, "Missing ANTHROPIC_API_KEY. Copy .env.example to .env and add your key."),
OPENAI_API_KEY: z.string().min(1).optional(),
});
const result = EnvSchema.safeParse(process.env);
if (!result.success) {
console.error("Invalid environment configuration:\n" + z.prettifyError(result.error));
process.exit(1);
}
export const env = result.data;Now every script fails fast with a message that says exactly what to fix. (Optional keys stay commented out in .env.example: an uncommented OPENAI_API_KEY= line sets the variable to an empty string, which fails the min(1) check.) The same file grows as you add providers and services: one place that lists every secret the project needs.
The shared AI module
Next, the one module every script imports. It creates the client once, defines model tiers so scripts never hard-code IDs, and includes a helper for the most common chore: pulling the text out of a response.
import Anthropic from "@anthropic-ai/sdk";
import { env } from "./env.js";
export const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
export const MODELS = {
fast: "claude-haiku-5-5",
default: "claude-sonnet-5-5",
best: "claude-opus-5-5",
} as const;
export type ModelTier = keyof typeof MODELS;
/** Join the text blocks of a response into one string. */
export function textOf(message: Anthropic.Message): string {
return message.content
.filter((block) => block.type === "text")
.map((block) => block.text)
.join("");
}A response's content is an array of content blocks, not a string. Most of the time you'll get text blocks, but the same array can hold thinking blocks or tool-use requests, so you filter for the type you want. Anatomy of a Model API Call explains every field.
Now the smoke test:
import { anthropic, MODELS, textOf } from "../lib/ai.js";
const response = await anthropic.messages.create({
model: MODELS.default,
max_tokens: 2000,
output_config: { effort: "low" },
system: "You are a helpful assistant inside a developer's test script. Answer in one short paragraph.",
messages: [{ role: "user", content: "In two sentences, what is a context window?" }],
});
console.log(textOf(response));
console.log("\nmodel:", response.model);
console.log("stop_reason:", response.stop_reason);
console.log("tokens:", response.usage.input_tokens, "in /", response.usage.output_tokens, "out");Run it with npm run hello. You should see a short answer, a stop_reason of end_turn (the model finished naturally), and a token count. The effort: "low" setting tells the model this is a simple request that doesn't need much reasoning, which keeps it fast and cheap; Module 1 covers effort properly.
A playground for trying every idea
The smoke test proves the plumbing works. The playground is what you'll actually use: a script that takes a system prompt file, an input file, a model tier, and an effort level, then prints the output with its stop reason, token counts, and latency.
// Usage: npm run try -- --system prompts/summarize.md --tier fast inputs/email.txt
import { readFile } from "node:fs/promises";
import { parseArgs } from "node:util";
import { anthropic, MODELS, textOf, type ModelTier } from "../lib/ai.js";
const { values, positionals } = parseArgs({
allowPositionals: true,
options: {
system: { type: "string" },
tier: { type: "string", default: "default" },
effort: { type: "string", default: "low" },
},
});
if (!(values.tier in MODELS)) throw new Error(`--tier must be one of: ${Object.keys(MODELS).join(", ")}`);
const efforts = ["low", "medium", "high", "xhigh", "max"] as const;
const effort = efforts.find((e) => e === values.effort);
if (!effort) throw new Error(`--effort must be one of: ${efforts.join(", ")}`);
const model = MODELS[values.tier as ModelTier];
const system = values.system ? await readFile(values.system, "utf8") : undefined;
const inputPath = positionals[0];
if (!inputPath) throw new Error("Pass the path of an input file as the last argument.");
const input = await readFile(inputPath, "utf8");
const started = Date.now();
const response = await anthropic.messages.create({
model,
max_tokens: 8000,
output_config: { effort },
system,
messages: [{ role: "user", content: input }],
});
console.log(textOf(response));
console.log(
`\n--- ${model} · effort ${effort} · ${response.stop_reason} · ` +
`${response.usage.input_tokens} in / ${response.usage.output_tokens} out · ${Date.now() - started} ms`,
);Prompts live in prompts/ and test inputs in inputs/, both as plain files. That separation is deliberate: you'll edit a prompt and rerun it against the same inputs dozens of times, and keeping them as files makes comparing runs (and later, turning them into eval datasets) trivial.
Try it:
npm run try -- --system prompts/summarize.md --tier fast inputs/email.txt
npm run try -- --system prompts/summarize.md --tier default inputs/email.txtRunning the same input on two tiers and reading both outputs side by side is one of the most useful experiments in AI building, and now it costs you one keystroke.
When something goes wrong
| Symptom | Likely cause | Fix |
|---|---|---|
.env: not found when running a script | No .env file in the folder you ran from | Run from the project root; copy .env.example to .env |
Missing ANTHROPIC_API_KEY | Variable name typo or empty value | Check spelling and that there are no quotes or spaces around the key |
| 401 authentication error | Key revoked, mistyped, or from another workspace | Create a new key and replace it in .env |
| 404 or "model not found" | Typo in a model ID, or a model your account can't use | Use the IDs from MODELS; never append date suffixes |
| 429 rate limit error | Too many requests or tokens per minute for your tier | Slow down; the SDK already retries twice with backoff |
Cannot find module '../lib/ai' | Missing .js extension under NodeNext | Import ../lib/ai.js |
Top-level await error | Project isn't an ES module | Run npm pkg set type=module |
Run npm run check after any change. Type errors are much cheaper to fix in your editor than as runtime surprises.
- Build the project above and get
npm run helloworking. - Create
prompts/summarize.mdwith a two-line system prompt: "Summarize the user's text in 3 bullet points. Keep names, numbers, and dates exact." - Create three files in
inputs/: a long email from your own inbox (remove anything sensitive), a short note, and a paragraph in another language. - Run all three through
npm run tryon thefastanddefaulttiers. Note the tokens and milliseconds for each run. - Add one improvement to
try.tsthat you'd find useful, such as saving each output to anoutputs/folder with a timestamp.
Success looks like: you can run any prompt against any input on any tier in one command, and you've noticed at least one difference in output quality or speed between tiers.
- Use Node.js 22 or newer, an ES module project, strict TypeScript with
NodeNext, and tsx to run files directly. - Keep keys in
.env, keep.envout of git, commit a.env.example, set a spend limit, and use separate keys per environment. - Validate configuration at startup so a missing key fails fast with a clear message.
- Put the client, model tiers, and response helpers in one shared module; scripts pick a tier, never a raw model ID.
- Keep prompts and inputs as files and use the playground to compare prompts, tiers, and effort levels in seconds.
Next: Your First AI Feature in 20 Minutes — turn this setup into a working "summarize this" endpoint and test it on messy input.