How to add Claude AI to any Node.js app in 5 minutes (with conversation memory)
Every Claude tutorial I've seen does one of two things:
- Shows you a single
curlrequest with no memory - Builds a massive framework with Redis, databases, and authentication
Both miss the point. Here's the exact code I use to add Claude with persistent conversation memory to any Node.js app — in under 50 lines, no database required.
The core problem: Claude has no memory by default
Each call to the Claude API is stateless. If you just do this:
const response = await anthropic.messages.create({
model: 'claude-opus-4-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'What did I just ask you?' }]
});
Claude has no idea what came before. Every request starts fresh.
The solution is to maintain a messages array and pass the full history with each request.
Complete implementation (47 lines)
const Anthropic = require('@anthropic-ai/sdk');
const readline = require('readline');
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
// This array is the entire "memory" — grow it, trim it, persist it however you want
const conversationHistory = [];
async function chat(userMessage) {
// Add the user's message to history
conversationHistory.push({
role: 'user',
content: userMessage
});
const response = await client.messages.create({
model: 'claude-opus-4-5',
max_tokens: 1024,
system: 'You are a helpful assistant.',
messages: conversationHistory // <-- pass the full history every time
});
const assistantMessage = response.content[0].text;
// Add Claude's response to history for next turn
conversationHistory.push({
role: 'assistant',
content: assistantMessage
});
return assistantMessage;
}
// Simple REPL to test it
async function main() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
console.log('Claude chatbot with memory. Type "quit" to exit.\n');
const ask = () => rl.question('You: ', async (input) => {
if (input.toLowerCase() === 'quit') return rl.close();
const reply = await chat(input);
console.log(`Claude: ${reply}\n`);
ask();
});
ask();
}
main();
Run it:
npm install @anthropic-ai/sdk
ANTHROPIC_API_KEY=sk-... node chat.js
Why this works
The conversationHistory array grows with every exchange. By the 5th message, you're sending Claude messages 1-4 as context. Claude reads them all and responds as if it remembers the entire conversation.
This is exactly how the official Claude.ai interface works — it's just an array passed with every request.
Problem: history grows forever
For a real app, you can't let the array grow unbounded — you'll hit token limits and slow down responses. Here's the trimming pattern I use:
const MAX_HISTORY_MESSAGES = 20; // Keep last 10 exchanges
async function chat(userMessage) {
conversationHistory.push({ role: 'user', content: userMessage });
// Trim to max length (always keep in pairs: user + assistant)
while (conversationHistory.length > MAX_HISTORY_MESSAGES) {
conversationHistory.splice(0, 2); // Remove oldest exchange
}
const response = await client.messages.create({
model: 'claude-opus-4-5',
max_tokens: 1024,
system: 'You are a helpful assistant.',
messages: conversationHistory
});
const assistantMessage = response.content[0].text;
conversationHistory.push({ role: 'assistant', content: assistantMessage });
return assistantMessage;
}
Always splice in pairs (remove 2 at a time) to keep the user/assistant/user/assistant alternating pattern that Claude requires.
Persist memory between sessions
Want memory to survive a server restart? Save the array to a file:
const fs = require('fs');
const HISTORY_FILE = './conversation.json';
// Load on startup
let conversationHistory = [];
if (fs.existsSync(HISTORY_FILE)) {
conversationHistory = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
}
async function chat(userMessage) {
conversationHistory.push({ role: 'user', content: userMessage });
// ... (trim + API call as before) ...
conversationHistory.push({ role: 'assistant', content: assistantMessage });
// Save after every exchange
fs.writeFileSync(HISTORY_FILE, JSON.stringify(conversationHistory, null, 2));
return assistantMessage;
}
For multi-user apps, key the history by session ID or user ID:
const histories = {};
async function chat(userId, userMessage) {
if (!histories[userId]) histories[userId] = [];
const history = histories[userId];
history.push({ role: 'user', content: userMessage });
// ... trim, API call, push response ...
return assistantMessage;
}
The flat-rate advantage for development
While building and testing this, I made hundreds of API calls iterating on the trimming logic, testing edge cases, and debugging the alternating role pattern. With per-token billing, that development phase would have cost real money.
I use SimplyLouie — a flat-rate Claude API wrapper at $2/month — specifically because I can hammer the API during development without watching a billing meter. The rate limits are generous enough for any solo project, and there's no invoice surprise at the end of the month.
For production apps at scale, you'd use official Anthropic API keys. But for prototyping, learning, and side projects, the flat-rate model removes a lot of anxiety.
What I'd do next
- Add streaming responses (see my previous article for the full streaming implementation)
- Add a system prompt that changes based on the user's context (detected language, subscription tier, etc.)
- Implement per-user rate limiting before the Claude call to prevent a single user from burning your quota
The full code above is 47 lines and runs with zero external dependencies beyond the Anthropic SDK.
What are you building with Claude? Drop it in the comments — I'm curious what real projects look like.
SimplyLouie is a $2/month flat-rate Claude API. 50% of revenue goes to animal rescue. simplylouie.com
Top comments (0)