Integrate the Claude API into your web application


Originally published on Bemorex Learn, our learning platform. · Leer en español
Introduction
The Claude API from Anthropic allows you to integrate advanced artificial intelligence into any web application. In this tutorial we will build a complete integration using native fetch(), without additional SDKs.
We will cover everything from the most basic call to advanced features like streaming and tool use.
Prerequisites
Before starting, you need:
- An account at console.anthropic.com
- An active API key
- Node.js 18+ (to use native
fetchon the backend)
Important: Never expose your API key on the frontend. Always make calls from your backend.
Your first API call
The Claude API uses the /v1/messages endpoint. Each call requires three mandatory headers:
Content-Type: application/jsonx-api-key: Your API keyanthropic-version: API version (use2023-06-01)
The body requires at minimum: model, max_tokens, and messages.
Available models
| Model | Ideal use | Speed | Capability |
|---|---|---|---|
| claude-opus-4-20250514 | Complex tasks, deep reasoning | Moderate | Maximum |
| claude-sonnet-4-20250514 | General balance | Fast | High |
| claude-haiku-3-5-20241022 | Fast tasks, high volume | Very fast | Good |
Response anatomy
The API response has this structure:
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "TypeScript is a superset of JavaScript..."
}
],
"model": "claude-sonnet-4-20250514",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 12,
"output_tokens": 156
}
}
Key points:
contentis an array (it can contain text and/or tool_use)usagetells you how many tokens you consumed (important for costs)stop_reasonindicates why the model stopped generating
Streaming: real-time responses
To improve the user experience, you can use streaming. Instead of waiting for the complete response, you receive tokens as they are generated.
You only need to add stream: true to the request body. The response will be a stream of Server-Sent Events (SSE).
Stream event types
| Event | Description |
|---|---|
message_start |
Start of the message with metadata |
content_block_start |
Start of a content block |
content_block_delta |
Text fragment (the actual content) |
content_block_stop |
End of the block |
message_delta |
Final update with stop_reason and usage |
message_stop |
End of the message |
System prompts
The system prompt defines the model's behavior. It is ideal for setting your application's context:
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: 'You are a programming tutor. Respond in English. Use code examples when relevant.',
messages: [
{ role: 'user', content: 'How do I use map in JavaScript?' }
]
})
});
Multi-turn conversations
To maintain a conversation, pass the entire message history:
const messages = [
{ role: 'user', content: 'What is a Promise?' },
{ role: 'assistant', content: 'A Promise is an object that represents...' },
{ role: 'user', content: 'Give me a practical example' }
];
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages
})
});
Claude will remember all the conversation context.
Tool Use: extend Claude's capabilities
Tool Use allows Claude to call functions that you define. This is extremely powerful for connecting Claude with external APIs, databases, or any business logic.
Tool Use flow
- You define available tools with their schema
- You send the user's question
- Claude decides whether it needs to use a tool
- If it does, it returns the name and parameters
- You execute the function and send the result back
- Claude generates the final response with that result
Production error handling
A robust wrapper for production must handle errors, retries, and rate limiting:
async function callClaudeWithRetry(messages, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages
})
});
if (response.status === 429) {
const retryAfter = response.headers.get('retry-after') || '30';
const waitMs = parseInt(retryAfter) * 1000;
console.log(`Rate limited. Waiting ${retryAfter}s...`);
await new Promise(r => setTimeout(r, waitMs));
continue;
}
if (response.status === 529) {
const waitMs = Math.pow(2, attempt) * 1000;
console.log(`API overloaded. Retrying in ${waitMs}ms...`);
await new Promise(r => setTimeout(r, waitMs));
continue;
}
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error ${response.status}: ${error.error.message}`);
}
return await response.json();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const waitMs = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, waitMs));
}
}
}
Security and best practices
Protect your API key
- Store it in environment variables, never in code
- Use a backend as a proxy between your frontend and the API
- Rotate keys periodically
Control costs
function estimateCost(inputTokens, outputTokens, model) {
const pricing = {
'claude-sonnet-4-20250514': { input: 3, output: 15 },
'claude-haiku-3-5-20241022': { input: 0.25, output: 1.25 }
};
const rates = pricing[model];
const inputCost = (inputTokens / 1_000_000) * rates.input;
const outputCost = (outputTokens / 1_000_000) * rates.output;
return { inputCost, outputCost, total: inputCost + outputCost };
}
Validate user input
Always sanitize and validate input before sending it to the API. Limit the maximum length and filter unwanted content.
Conclusion
Integrating the Claude API with native fetch() is straightforward and requires no additional dependencies. The key concepts are: basic calls, streaming for better UX, tool use to extend capabilities, and robust error handling for production.
Start with simple calls, add streaming when you improve the UX, and explore tool use when you need to connect Claude with your systems.
Code examples
JavaScript
// Basic call to the Claude API with fetch
async function askClaude(question) {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [
{ role: 'user', content: question }
]
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${error.error.message}`);
}
const data = await response.json();
return data.content[0].text;
}
// Usage
const answer = await askClaude('What is TypeScript?');
console.log(answer);JavaScript
// Streaming responses with fetch + ReadableStream
async function streamClaude(question, onChunk) {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
stream: true,
messages: [
{ role: 'user', content: question }
]
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n').filter(l => l.startsWith('data: '));
for (const line of lines) {
const json = JSON.parse(line.slice(6));
if (json.type === 'content_block_delta') {
onChunk(json.delta.text);
}
}
}
}
// Usage: display text progressively
let fullResponse = '';
await streamClaude('Explain async/await', (chunk) => {
fullResponse += chunk;
process.stdout.write(chunk);
});JavaScript
// Tool Use: Claude can call functions you define
async function claudeWithTools(question) {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
tools: [{
name: 'get_weather',
description: 'Gets the current weather for a city',
input_schema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name'
}
},
required: ['city']
}
}],
messages: [
{ role: 'user', content: question }
]
})
});
const data = await response.json();
// Check whether Claude wants to use a tool
const toolUse = data.content.find(c => c.type === 'tool_use');
if (toolUse) {
console.log(`Claude wants to call: ${toolUse.name}`);
console.log(`With parameters:`, toolUse.input);
}
return data;
}