June 19, 2026
1 min read
SSE is what the Claude API uses natively. Wrapping the Anthropic SDK stream in an RxJS Observable behind NestJS's @Sse() decorator gives token-by-token delivery in ~30 lines.
When you put an LLM behind your own API, you almost always want to stream the response — waiting 20 seconds for a full completion makes the UI feel broken. Server-Sent Events (SSE) are the simplest transport: one-way, text-based, and supported by every browser through EventSource. It is also what the Claude and OpenAI APIs use natively.
EventSource reconnects automaticallyNestJS has first-class SSE support through the @Sse() decorator, which returns an Observable:
import { Sse, MessageEvent, Query } from '@nestjs/common';
import { Observable } from 'rxjs';
@Sse('chat')
chat(@Query('prompt') prompt: string): Observable<MessageEvent> {
return this.llmService.streamReply(prompt);
}
Wrap the Anthropic SDK's stream in an RxJS Observable:
import Anthropic from '@anthropic-ai/sdk';
@Injectable()
export class LlmService {
private client = new Anthropic();
streamReply(prompt: string): Observable<MessageEvent> {
return new Observable((subscriber) => {
const stream = this.client.messages.stream({
model: 'claude-opus-5',
max_tokens: 4096,
messages: [{ role: 'user', content: prompt }],
});
stream.on('text', (chunk) => subscriber.next({ data: chunk }));
stream.on('end', () => subscriber.complete());
stream.on('error', (err) => subscriber.error(err));
return () => stream.abort(); // runs when the client disconnects
});
}
}
const source = new EventSource(/llm/chat?prompt=${encodeURIComponent(input)});
source.onmessage = (e) => appendToOutput(e.data);
source.onerror = () => source.close();
max_tokens bounded — a runaway generation holds a connection openThe @Sse() decorator plus an Observable wrapper gives you token-by-token delivery in about 30 lines, with cancellation and backpressure handled by RxJS.