In lesson 1 you built the skeleton of the AI Lab. Now we bring it to life. In this lesson you will get an API key, store it the safe way, and send your first real question to Claude. At the end, a user will type a question in your Angular page and see Claude's answer.
Step 1: Get an API key
An API key is a long secret text that proves a request comes from your account. It works like the key to your shop: whoever holds it can use your account and spend your money. To get one, create an account in the Anthropic Console, add a payment method or credits as required, and create a key. The console shows the key only once. Copy it and keep it somewhere safe right away, because you cannot view it again later. If you lose it, you simply create a new one.
Create separate keys for development and for production. If a development key leaks, you can delete it without breaking your live site. Give each key a clear name such as "ai-lab-dev" so you remember what it is for.
Step 2: Keep the key secret
Most key leaks happen by accident. Someone puts a key in the source code, pushes the code to GitHub, and bots find it within minutes. Follow these simple rules every time.
- Store the key in a file called .env on the server. The file has lines like ANTHROPIC_API_KEY=your-key-here.
- Add .env to your .gitignore file so Git never uploads it. Commit a file called .env.example instead, with the names but no real values.
- Never put the key in Angular code or in any file that goes to the browser.
- If a key ever appears in a public place, delete it in the console and create a new one immediately.
- On a real server (such as your VPS), set the key as an environment variable, not in a file inside the project folder.
The AI Lab already has these protections. Look at server/index.js from lesson 1: the first line imports dotenv, which reads the .env file and puts its values into process.env, where the rest of the server can use them.
Step 3: The SDK
You could call the Claude API with plain HTTP requests, but Anthropic provides an SDK, a ready-made library, for JavaScript and TypeScript. It is called @anthropic-ai/sdk. It handles the details for you: the right web address, the headers, the formatting of messages, and even some automatic retries when the network has a small problem. In the AI Lab it is already listed in package.json, so npm install gets it for you.
Creating the client takes one line: new Anthropic(). It reads the key from the ANTHROPIC_API_KEY environment variable by itself. That is why our file server/claude.js stays so small.
Step 4: Your first message
Here is the heart of every Claude request, using the Messages API.
const reply = await client.messages.create({
model: MODEL,
max_tokens: 500,
messages: [{ role: 'user', content: 'Give me three tips for a fresher\'s resume.' }],
});
console.log(reply.content[0].text);
Let us read it slowly, because you will write this many times.
- model says which Claude model to use. We take it from the .env file.
- max_tokens is the longest answer you accept, counted in tokens. It is required. It is a ceiling, not a target, and it protects your bill: even if Claude wants to write a very long answer, it stops at this limit.
- messages is a list. Each message has a role and some content. The role "user" means the message comes from the person asking. Here we send one message.
The reply is an object. Its content is a list of blocks, because an answer can contain more than text (later you will see tool requests too). For a simple answer there is one text block, and its text is in reply.content[0].text. To be safe in real code, we join all the text blocks together. That is what the small helper textOf() in server/claude.js does. The reply also has usage, which tells you how many tokens went in and came out, and stop_reason, which tells you why Claude stopped (for example, it finished, or it hit your max_tokens limit).
Check what comes from the browser
Your server is a front door. Anyone on the internet can send anything to it, so check every input. In our route we do three small things. We turn the question into text, trim the spaces, and cut it to 2,000 characters so nobody can send a book and make you pay for it. If the question is empty, we answer with a clear 400 error that says "Please type a question". Good validation is cheap and prevents expensive surprises.
Show a friendly error
Many things can go wrong: the key is missing, the internet is down, Claude is busy. In lesson 1 we added one error handler at the bottom of server/index.js. It writes the full error to the server log for you, but sends only a short, safe message to the browser. Never show raw error text to users. It can contain details that help attackers and it confuses normal people. In Angular, our small helper errorText() in util.ts reads that safe message and shows it.
How Angular sends the question
On the Angular side we use HttpClient, the built-in tool for talking to servers. The page keeps its state in signals: loading, error and reply. A signal is a small box that holds a value. When the value changes, Angular updates the screen by itself. The flow is simple. When the user presses Ask, the page sets loading to true, posts the question to /api/ask, waits for the answer, stores it in the reply signal, and sets loading back to false. The finally block makes sure loading is turned off even if something fails.
Your task: build the Ask page
Add a page where a user types a question and gets Claude's answer, with a loading state and a friendly error message.
What you will build
- A POST /api/ask route on the server that validates the question, calls Claude, and returns the answer text, the token usage, and the stop reason.
- An Ask page in Angular with a text area, an Ask button that is disabled while loading, the answer, and the token usage under it.
- A shared errorText() helper that turns any HTTP error into one friendly sentence.
Steps
- Start from your lesson 1 project, or download the lesson 1 solution.
- Create your API key and put it into .env as ANTHROPIC_API_KEY. Restart the server so it reads the file.
- Create server/routes/ask.js with the POST /ask route. Validate the question (not empty, at most 2,000 characters). Call client.messages.create with max_tokens 500.
- Return JSON with text, usage and stopReason.
- Create src/app/util.ts with errorText(). It should return the server's message if it exists, or a general sentence if not.
- Create AskPage with signals for loading, error and reply. Add it to the routes and to the menu.
- Try a normal question. Then try an empty question, then stop the server and press Ask again, and see that the error is friendly.
Hints
- Use firstValueFrom(this.http.post(...)) to use async and await with HttpClient.
- Show the usage like "Used 20 input and 12 output tokens" so you start to notice how short answers are cheap.
- If you get an authentication error, check that the key is correct, that you restarted the server, and that your account has credits.
You know it works when a real answer appears, the button shows "Thinking..." while waiting, an empty question gives a clear message, and no key or raw error text ever appears in the browser.
Solution
The lesson 2 zip is the lesson 1 project plus the files below. The server route:
import { Router } from 'express';
import { client, MODEL, textOf } from '../claude.js';
const router = Router();
// POST /api/ask { question } -> { text, usage, stopReason }
router.post('/ask', async (req, res) => {
const question = String(req.body?.question || '').trim().slice(0, 2000);
if (!question) {
return res.status(400).json({ error: { message: 'Please type a question.' } });
}
const reply = await client.messages.create({
model: MODEL,
max_tokens: 500,
messages: [{ role: 'user', content: question }],
});
res.json({ text: textOf(reply), usage: reply.usage, stopReason: reply.stop_reason });
});
export default router;
The Angular page:
import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { errorText } from '../util';
type AskReply = { text: string; usage: { input_tokens: number; output_tokens: number }; stopReason: string };
@Component({
selector: 'app-ask',
imports: [FormsModule],
template: `
<h1>Ask Claude</h1>
<p class="hint">Your first real request. The browser talks to our server, and the server talks to Claude. The API key never reaches the browser.</p>
<div class="card">
<label for="q">Your question</label>
<textarea id="q" [(ngModel)]="question" placeholder="Give me three tips for a fresher's resume."></textarea>
<button (click)="send()" [disabled]="loading() || !question.trim()">{{ loading() ? 'Thinking...' : 'Ask' }}</button>
@if (error()) { <div class="err">{{ error() }}</div> }
@if (reply(); as r) {
<div class="answer">{{ r.text }}</div>
<p class="muted">Used {{ r.usage.input_tokens }} input and {{ r.usage.output_tokens }} output tokens.</p>
}
</div>
`,
})
export class AskPage {
private http = inject(HttpClient);
question = '';
loading = signal(false);
error = signal('');
reply = signal<AskReply | null>(null);
async send() {
this.loading.set(true);
this.error.set('');
this.reply.set(null);
try {
this.reply.set(await firstValueFrom(this.http.post<AskReply>('/api/ask', { question: this.question })));
} catch (e) {
this.error.set(errorText(e));
} finally {
this.loading.set(false);
}
}
}
The shared helper (it was added in the base files, so it is already in the zip):
// Turn any HTTP error into one friendly sentence for the screen.
export function errorText(e: unknown): string {
const err = e as { error?: { error?: { message?: string } }; message?: string };
return err?.error?.error?.message || 'Something went wrong. Please try again.';
}
Download the lesson 2 solution (zip)
What you learned
- An API key is a secret. Keep it in .env on the server, and keep .env out of Git.
- The Messages API takes a model, max_tokens and a list of messages, and returns content blocks, usage and a stop reason.
- Validate every input and show friendly errors; never show raw server errors.
- Angular signals hold loading, error and result state in a clean way.
Next, lesson 3 shows how tokens are counted, how to estimate cost, and how to protect your budget.