Azure AI services pair naturally with NestJS. Here’s how I’ve been integrating them in production apps.
Setting Up the Azure OpenAI Module
First, create a dedicated module. NestJS’s dependency injection makes it clean:
// azure-ai.module.ts
@Module({
providers: [
{
provide: 'AZURE_OPENAI_CLIENT',
useFactory: () => {
return new AzureOpenAI({
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_KEY,
apiVersion: '2024-10-21',
});
},
},
AzureAiService,
],
exports: [AzureAiService],
})
export class AzureAiModule {}
Streaming Responses with SSE
For chat features, Server-Sent Events give you that ChatGPT-like streaming experience:
@Controller('ai')
export class AiController {
constructor(private aiService: AzureAiService) {}
@Post('chat')
@Sse()
async chat(@Body() dto: ChatDto): Promise<Observable<MessageEvent>> {
return new Observable((subscriber) => {
this.aiService.streamChat(dto.messages).then(async (stream) => {
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
subscriber.next({ data: JSON.stringify({ content }) });
}
}
subscriber.complete();
});
});
}
}
Azure AI Search for RAG
The real power comes when you combine Azure OpenAI with Azure AI Search:
async searchAndAnswer(query: string): Promise<string> {
// 1. Search relevant documents
const searchResults = await this.searchClient.search(query, {
queryType: 'semantic',
semanticSearchOptions: {
configurationName: 'my-semantic-config',
},
top: 5,
});
// 2. Build context from results
const context = searchResults.results
.map(r => r.document.content)
.join('\n\n');
// 3. Generate answer with context
const response = await this.openaiClient.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: `Answer based on this context:\n${context}` },
{ role: 'user', content: query },
],
});
return response.choices[0].message.content;
}
Document Intelligence for PDF Processing
Azure Document Intelligence (formerly Form Recognizer) is incredible for extracting structured data from PDFs:
async processInvoice(fileBuffer: Buffer): Promise<InvoiceData> {
const poller = await this.diClient.beginAnalyzeDocument(
'prebuilt-invoice',
fileBuffer
);
const result = await poller.pollUntilDone();
return {
vendor: result.documents[0].fields.VendorName?.content,
total: result.documents[0].fields.InvoiceTotal?.value,
date: result.documents[0].fields.InvoiceDate?.value,
lineItems: result.documents[0].fields.Items?.values.map(item => ({
description: item.fields.Description?.content,
amount: item.fields.Amount?.value,
})),
};
}
Error Handling & Retry
Azure services can be flaky. Always add retry logic:
// Use the built-in NestJS retry with rxjs
import { retry, timer } from 'rxjs';
this.aiService.generateResponse(prompt).pipe(
retry({
count: 3,
delay: (error, retryCount) => timer(retryCount * 1000),
})
);
Cost Management
Azure AI costs add up fast. Three tips:
- Cache embeddings — don’t re-embed unchanged documents
- Use GPT-4o-mini for classification and simple tasks, GPT-4o for complex reasoning
- Set token limits — always pass
max_tokensto prevent runaway costs
The NestJS + Azure AI combo is production-solid. The DI system keeps your AI services testable and the modular architecture makes it easy to swap providers later.