Skip to content
47% off: the complete course for $79 (reg. $149)Order now →
AI Builder
Module 0Free lesson9 min read

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.

PieceWhat it isWhy you want it
Node.js 22+The JavaScript runtimeModern ES modules, top-level await, built-in .env loading
TypeScript (strict)Types for JavaScriptCatches the classic bug of reading .text on a block that isn't text
tsxRuns .ts files directlyNo build step between editing and trying
@anthropic-ai/sdkAnthropic's official TypeScript SDKTyped requests and responses, retries, streaming
ZodA schema libraryValidates config now; validates model output from Module 4 on
.env + validationKeys outside your code, checked at startupNo leaked keys, no mysterious 401 errors mid-experiment
The playgroundsrc/play/try.tsTry 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 --version

If 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 inputs

npm 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:

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:

package.json (scripts)
{
  "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:

.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-key

Keep it out of git, and commit a template instead so others (and future you) know which variables exist:

.gitignore
node_modules
.env
.env.*
!.env.example
.env.example
ANTHROPIC_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:

src/lib/env.ts
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.

src/lib/ai.ts
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:

src/play/hello.ts
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.

src/play/try.ts
// 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.txt

Running 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

SymptomLikely causeFix
.env: not found when running a scriptNo .env file in the folder you ran fromRun from the project root; copy .env.example to .env
Missing ANTHROPIC_API_KEYVariable name typo or empty valueCheck spelling and that there are no quotes or spaces around the key
401 authentication errorKey revoked, mistyped, or from another workspaceCreate 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 useUse the IDs from MODELS; never append date suffixes
429 rate limit errorToo many requests or tokens per minute for your tierSlow down; the SDK already retries twice with backoff
Cannot find module '../lib/ai'Missing .js extension under NodeNextImport ../lib/ai.js
Top-level await errorProject isn't an ES moduleRun 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.

ExerciseMake the playground yours
  1. Build the project above and get npm run hello working.
  2. Create prompts/summarize.md with a two-line system prompt: "Summarize the user's text in 3 bullet points. Keep names, numbers, and dates exact."
  3. Create three files in inputs/: a long email from your own inbox (remove anything sensitive), a short note, and a paragraph in another language.
  4. Run all three through npm run try on the fast and default tiers. Note the tokens and milliseconds for each run.
  5. Add one improvement to try.ts that you'd find useful, such as saving each output to an outputs/ 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.

Key takeaways
  • 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 .env out 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.