Skip to main content

Anthropic (Claude) Integration

Initializing the Client

src/lib/anthropic.js
import Anthropic from '@anthropic-ai/sdk';
import { ANTHROPIC_API_KEY } from '../config/env.js';

const anthropic = new Anthropic({
apiKey: ANTHROPIC_API_KEY,
timeout: 60_000,
maxRetries: 2,
});

export default anthropic;

Messages API

Anthropic's API has two notable differences from OpenAI:

  1. system is a top-level field, not a message role inside the array
  2. max_tokens is required — there is no default
src/services/anthropic.service.js
import anthropic from '../lib/anthropic.js';

export async function chat({
messages,
model = 'claude-sonnet-4-6',
systemPrompt = '',
maxTokens = 1024,
temperature = 1, // Anthropic default is 1; range is 0–1
}) {
const response = await anthropic.messages.create({
model,
max_tokens: maxTokens,
temperature,
system: systemPrompt,
messages,
});

return {
content: response.content[0].text,
usage: response.usage,
stopReason: response.stop_reason,
};
}

stop_reason values:

ValueMeaning
end_turnNormal completion
max_tokensHit the max_tokens limit — response cut off
stop_sequenceA configured stop sequence was encountered
tool_useModel wants to call a tool

Streaming with SSE

src/routes/claude.route.js — streaming
import anthropic from '../lib/anthropic.js';

router.post('/stream', async (req, res, next) => {
try {
const { messages, systemPrompt } = req.body;

res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');

const stream = anthropic.messages.stream({
model: 'claude-sonnet-4-6',
max_tokens: 1024,
system: systemPrompt || '',
messages,
});

for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
res.write(`data: ${JSON.stringify({ token: event.delta.text })}\n\n`);
}
if (event.type === 'message_stop') {
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
}
}

res.end();
} catch (err) {
if (res.headersSent) {
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
res.end();
} else {
next(err);
}
}
});

Anthropic SSE event types:

Event typeWhen it fires
message_startStart of the response with initial metadata
content_block_startA new content block begins (text or tool)
content_block_deltaA token delta — the actual streamed text
content_block_stopA content block ends
message_deltaFinal usage stats
message_stopStream fully complete

Vision with Claude

Sending an image URL to Claude
const response = await anthropic.messages.create({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'image',
source: { type: 'url', url: 'https://example.com/diagram.png' },
},
{ type: 'text', text: 'Explain this architecture diagram.' },
],
},
],
});
Sending a base64 image to Claude
const response = await anthropic.messages.create({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [
{
role: 'user',
content: [
{
type: 'image',
source: {
type: 'base64',
media_type: 'image/jpeg', // 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp'
data: base64String,
},
},
{ type: 'text', text: 'What does this show?' },
],
},
],
});

Structured Output

Claude does not have a native JSON schema enforcement mode, but it follows JSON instructions reliably when prompted correctly:

JSON extraction with Claude
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 512,
system: 'You are a data extraction assistant. Always respond with valid JSON only. No explanation, no markdown, just JSON.',
messages: [
{
role: 'user',
content: 'Extract name and email from: "Contact Jane at jane@example.com"',
},
],
});

const data = JSON.parse(response.content[0].text);