Muhammed Ulvi Özkaya
Blog
Blog
NestJSBackendTypeScript

Streaming Claude Responses Through NestJS with Server-Sent Events

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.

Why SSE Instead of WebSockets

  • LLM output is one-directional — the client sends nothing mid-stream
  • Runs over plain HTTP, no upgrade handshake
  • EventSource reconnects automatically

The Controller

NestJS 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);
}

The Service

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
});
}
}

Consuming It

const source = new EventSource(/llm/chat?prompt=${encodeURIComponent(input)});
source.onmessage = (e) => appendToOutput(e.data);
source.onerror = () => source.close();

Things to Watch

  • Keep max_tokens bounded — a runaway generation holds a connection open
  • Abort the upstream stream on disconnect (the cleanup function above)
  • Rate-limit the endpoint; every open stream consumes a connection and a provider quota slot

Conclusion

The @Sse() decorator plus an Observable wrapper gives you token-by-token delivery in about 30 lines, with cancellation and backpressure handled by RxJS.


© 2026 Muhammed Ulvi Ozkaya. All rights reserved.
Privacy Policy