Picture this: You're building the next breakthrough AI application, but your JavaScript code crashes at runtime because a model response came back undefined. Sound familiar? Welcome to the wild west of AI development without type safety.
Building reliable AI applications requires more than just connecting to models—it demands robust type safety, proper error handling, and maintainable code architecture. TypeScript integration with Ollama transforms chaotic AI development into a structured, predictable process that catches errors before they reach production.
This comprehensive guide demonstrates how to build production-ready TypeScript applications using Ollama's local AI models. You'll learn type definitions, error handling patterns, and architectural best practices that ensure your AI applications run smoothly in production environments.
Understanding TypeScript and Ollama Integration Benefits
Why TypeScript Matters for AI Development
TypeScript provides compile-time type checking that prevents common runtime errors in AI applications. Traditional JavaScript AI development often fails due to:
- Undefined model responses breaking application flow
- Incorrect parameter types causing API failures
- Missing error handling for network timeouts
- Inconsistent data structures across different models
TypeScript eliminates these issues by enforcing strict typing throughout your application lifecycle.
Ollama's Local AI Advantage
Ollama runs AI models locally on your machine, providing:
- Privacy: No data leaves your environment
- Speed: Reduced latency compared to cloud APIs
- Cost: No per-request charges
- Reliability: Works offline without internet dependency
Combining TypeScript's type safety with Ollama's local execution creates robust AI applications that developers can trust.
Setting Up Your TypeScript Ollama Environment
Installation and Initial Configuration
First, install the necessary dependencies for your TypeScript Ollama project:
# Create new TypeScript project
mkdir typescript-ollama-app
cd typescript-ollama-app
npm init -y
# Install TypeScript and Ollama dependencies
npm install ollama
npm install -D typescript @types/node tsx nodemon
# Initialize TypeScript configuration
npx tsc --init
Configure your tsconfig.json for optimal development experience:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Creating Type-Safe Ollama Client
Build a strongly-typed Ollama client that provides type safety across your entire application:
// src/ollama-client.ts
import { Ollama } from 'ollama';
// Define response interfaces for type safety
export interface ChatResponse {
model: string;
created_at: string;
message: {
role: 'assistant' | 'user' | 'system';
content: string;
};
done: boolean;
total_duration?: number;
load_duration?: number;
prompt_eval_count?: number;
prompt_eval_duration?: number;
eval_count?: number;
eval_duration?: number;
}
export interface ChatRequest {
model: string;
messages: Array<{
role: 'user' | 'assistant' | 'system';
content: string;
}>;
stream?: boolean;
options?: {
temperature?: number;
top_p?: number;
top_k?: number;
repeat_penalty?: number;
seed?: number;
num_ctx?: number;
};
}
export interface ModelInfo {
name: string;
modified_at: string;
size: number;
digest: string;
details: {
format: string;
family: string;
families: string[];
parameter_size: string;
quantization_level: string;
};
}
export class TypedOllamaClient {
private client: Ollama;
constructor(host: string = 'http://localhost:11434') {
this.client = new Ollama({ host });
}
async chat(request: ChatRequest): Promise<ChatResponse> {
try {
const response = await this.client.chat(request);
return response as ChatResponse;
} catch (error) {
throw new Error(`Chat request failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async listModels(): Promise<ModelInfo[]> {
try {
const response = await this.client.list();
return response.models as ModelInfo[];
} catch (error) {
throw new Error(`Failed to list models: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async pullModel(modelName: string): Promise<void> {
try {
await this.client.pull({ model: modelName });
} catch (error) {
throw new Error(`Failed to pull model ${modelName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async generateResponse(
model: string,
prompt: string,
options?: ChatRequest['options']
): Promise<string> {
const request: ChatRequest = {
model,
messages: [{ role: 'user', content: prompt }],
options
};
const response = await this.chat(request);
return response.message.content;
}
}
Building Type-Safe AI Applications
Creating a Conversational AI Service
Develop a service class that handles conversations with proper type safety and error handling:
// src/conversation-service.ts
import { TypedOllamaClient, ChatRequest } from './ollama-client';
export interface ConversationMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
metadata?: {
model?: string;
duration?: number;
tokens?: number;
};
}
export interface ConversationHistory {
id: string;
messages: ConversationMessage[];
created_at: Date;
updated_at: Date;
model: string;
}
export class ConversationService {
private client: TypedOllamaClient;
private conversations: Map<string, ConversationHistory> = new Map();
constructor(client: TypedOllamaClient) {
this.client = client;
}
createConversation(model: string, systemPrompt?: string): string {
const conversationId = this.generateId();
const conversation: ConversationHistory = {
id: conversationId,
messages: [],
created_at: new Date(),
updated_at: new Date(),
model
};
if (systemPrompt) {
conversation.messages.push({
id: this.generateId(),
role: 'system',
content: systemPrompt,
timestamp: new Date()
});
}
this.conversations.set(conversationId, conversation);
return conversationId;
}
async sendMessage(
conversationId: string,
message: string,
options?: ChatRequest['options']
): Promise<ConversationMessage> {
const conversation = this.conversations.get(conversationId);
if (!conversation) {
throw new Error(`Conversation ${conversationId} not found`);
}
// Add user message to conversation
const userMessage: ConversationMessage = {
id: this.generateId(),
role: 'user',
content: message,
timestamp: new Date()
};
conversation.messages.push(userMessage);
// Prepare request with conversation history
const request: ChatRequest = {
model: conversation.model,
messages: conversation.messages.map(msg => ({
role: msg.role,
content: msg.content
})),
options
};
try {
const startTime = Date.now();
const response = await this.client.chat(request);
const duration = Date.now() - startTime;
// Add assistant response to conversation
const assistantMessage: ConversationMessage = {
id: this.generateId(),
role: 'assistant',
content: response.message.content,
timestamp: new Date(),
metadata: {
model: response.model,
duration,
tokens: response.eval_count
}
};
conversation.messages.push(assistantMessage);
conversation.updated_at = new Date();
return assistantMessage;
} catch (error) {
throw new Error(`Failed to send message: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
getConversation(conversationId: string): ConversationHistory | undefined {
return this.conversations.get(conversationId);
}
getConversationHistory(conversationId: string): ConversationMessage[] {
const conversation = this.conversations.get(conversationId);
return conversation ? [...conversation.messages] : [];
}
private generateId(): string {
return Math.random().toString(36).substr(2, 9);
}
}
Implementing Error Handling and Validation
Create robust error handling that provides meaningful feedback:
// src/error-handler.ts
export enum OllamaErrorType {
CONNECTION_ERROR = 'CONNECTION_ERROR',
MODEL_NOT_FOUND = 'MODEL_NOT_FOUND',
INVALID_REQUEST = 'INVALID_REQUEST',
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
RATE_LIMIT_ERROR = 'RATE_LIMIT_ERROR',
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
}
export class OllamaError extends Error {
constructor(
public type: OllamaErrorType,
message: string,
public originalError?: Error
) {
super(message);
this.name = 'OllamaError';
}
}
export class ErrorHandler {
static handleOllamaError(error: unknown): OllamaError {
if (error instanceof OllamaError) {
return error;
}
if (error instanceof Error) {
// Parse common error patterns
if (error.message.includes('ECONNREFUSED')) {
return new OllamaError(
OllamaErrorType.CONNECTION_ERROR,
'Cannot connect to Ollama server. Ensure Ollama is running.',
error
);
}
if (error.message.includes('model not found')) {
return new OllamaError(
OllamaErrorType.MODEL_NOT_FOUND,
'The requested model is not available. Pull the model first.',
error
);
}
if (error.message.includes('timeout')) {
return new OllamaError(
OllamaErrorType.TIMEOUT_ERROR,
'Request timed out. The model may be processing a complex request.',
error
);
}
}
return new OllamaError(
OllamaErrorType.UNKNOWN_ERROR,
'An unexpected error occurred',
error instanceof Error ? error : new Error(String(error))
);
}
static async withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === maxRetries) {
throw ErrorHandler.handleOllamaError(lastError);
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
throw ErrorHandler.handleOllamaError(lastError!);
}
}
Advanced TypeScript Patterns for AI Applications
Creating Generic Model Interfaces
Design flexible interfaces that work with different AI models:
// src/model-interfaces.ts
export interface BaseModelConfig {
model: string;
temperature?: number;
maxTokens?: number;
topP?: number;
topK?: number;
repeatPenalty?: number;
}
export interface ModelCapabilities {
supportsChat: boolean;
supportsCompletion: boolean;
supportsEmbedding: boolean;
contextWindow: number;
languages: string[];
}
export interface ModelMetrics {
requestCount: number;
totalDuration: number;
averageDuration: number;
errorRate: number;
lastUsed: Date;
}
export abstract class BaseModel<TConfig extends BaseModelConfig> {
protected config: TConfig;
protected metrics: ModelMetrics;
constructor(config: TConfig) {
this.config = config;
this.metrics = {
requestCount: 0,
totalDuration: 0,
averageDuration: 0,
errorRate: 0,
lastUsed: new Date()
};
}
abstract getCapabilities(): ModelCapabilities;
abstract generateResponse(prompt: string): Promise<string>;
getMetrics(): ModelMetrics {
return { ...this.metrics };
}
protected updateMetrics(duration: number, success: boolean): void {
this.metrics.requestCount++;
this.metrics.lastUsed = new Date();
if (success) {
this.metrics.totalDuration += duration;
this.metrics.averageDuration = this.metrics.totalDuration / this.metrics.requestCount;
} else {
this.metrics.errorRate = (this.metrics.errorRate * (this.metrics.requestCount - 1) + 1) / this.metrics.requestCount;
}
}
}
// Specific implementation for Ollama models
export interface OllamaModelConfig extends BaseModelConfig {
host?: string;
keepAlive?: string;
numCtx?: number;
}
export class OllamaModel extends BaseModel<OllamaModelConfig> {
private client: TypedOllamaClient;
constructor(config: OllamaModelConfig) {
super(config);
this.client = new TypedOllamaClient(config.host);
}
getCapabilities(): ModelCapabilities {
return {
supportsChat: true,
supportsCompletion: true,
supportsEmbedding: false,
contextWindow: this.config.numCtx || 4096,
languages: ['en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh']
};
}
async generateResponse(prompt: string): Promise<string> {
const startTime = Date.now();
let success = false;
try {
const response = await this.client.generateResponse(
this.config.model,
prompt,
{
temperature: this.config.temperature,
top_p: this.config.topP,
top_k: this.config.topK,
repeat_penalty: this.config.repeatPenalty,
num_ctx: this.config.numCtx
}
);
success = true;
return response;
} finally {
const duration = Date.now() - startTime;
this.updateMetrics(duration, success);
}
}
}
Building a Complete Application Example
Create a comprehensive CLI application that demonstrates all concepts:
// src/app.ts
import { TypedOllamaClient } from './ollama-client';
import { ConversationService } from './conversation-service';
import { ErrorHandler, OllamaError } from './error-handler';
import { OllamaModel } from './model-interfaces';
import * as readline from 'readline';
class OllamaApp {
private client: TypedOllamaClient;
private conversationService: ConversationService;
private model: OllamaModel;
private rl: readline.Interface;
constructor() {
this.client = new TypedOllamaClient();
this.conversationService = new ConversationService(this.client);
this.model = new OllamaModel({
model: 'llama3.2:3b',
temperature: 0.7,
maxTokens: 2048
});
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
}
async initialize(): Promise<void> {
console.log('🚀 Initializing TypeScript Ollama Application...\n');
try {
// Check available models
const models = await this.client.listModels();
console.log('📦 Available models:');
models.forEach(model => {
console.log(` - ${model.name} (${this.formatSize(model.size)})`);
});
// Ensure our model is available
const modelExists = models.some(m => m.name === this.model.config.model);
if (!modelExists) {
console.log(`\n⏳ Pulling model: ${this.model.config.model}`);
await this.client.pullModel(this.model.config.model);
}
console.log('\n✅ Application initialized successfully!\n');
} catch (error) {
const ollamaError = ErrorHandler.handleOllamaError(error);
console.error(`❌ Initialization failed: ${ollamaError.message}\n`);
process.exit(1);
}
}
async startConversation(): Promise<void> {
const conversationId = this.conversationService.createConversation(
this.model.config.model,
'You are a helpful AI assistant built with TypeScript and Ollama.'
);
console.log('💬 Starting conversation. Type "quit" to exit, "stats" for metrics.\n');
while (true) {
const input = await this.prompt('You: ');
if (input.toLowerCase() === 'quit') {
break;
}
if (input.toLowerCase() === 'stats') {
this.displayStats();
continue;
}
try {
console.log('🤖 Assistant: Thinking...');
const response = await ErrorHandler.withRetry(
() => this.conversationService.sendMessage(conversationId, input),
3,
1000
);
console.log(`🤖 Assistant: ${response.content}\n`);
} catch (error) {
const ollamaError = ErrorHandler.handleOllamaError(error);
console.error(`❌ Error: ${ollamaError.message}\n`);
}
}
}
private async prompt(question: string): Promise<string> {
return new Promise((resolve) => {
this.rl.question(question, (answer) => {
resolve(answer);
});
});
}
private displayStats(): void {
const metrics = this.model.getMetrics();
console.log('\n📊 Model Statistics:');
console.log(` Requests: ${metrics.requestCount}`);
console.log(` Average Duration: ${metrics.averageDuration.toFixed(2)}ms`);
console.log(` Error Rate: ${(metrics.errorRate * 100).toFixed(2)}%`);
console.log(` Last Used: ${metrics.lastUsed.toLocaleString()}\n`);
}
private formatSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
close(): void {
this.rl.close();
}
}
// Application entry point
async function main(): Promise<void> {
const app = new OllamaApp();
try {
await app.initialize();
await app.startConversation();
} catch (error) {
console.error('Application error:', error);
} finally {
app.close();
}
}
// Run the application
if (require.main === module) {
main().catch(console.error);
}
Testing Your TypeScript Ollama Application
Unit Testing with Jest
Create comprehensive tests for your TypeScript Ollama integration:
// src/__tests__/ollama-client.test.ts
import { TypedOllamaClient } from '../ollama-client';
import { ErrorHandler } from '../error-handler';
describe('TypedOllamaClient', () => {
let client: TypedOllamaClient;
beforeEach(() => {
client = new TypedOllamaClient();
});
describe('generateResponse', () => {
it('should generate a response with proper typing', async () => {
const response = await client.generateResponse(
'llama3.2:3b',
'Hello, world!'
);
expect(typeof response).toBe('string');
expect(response.length).toBeGreaterThan(0);
});
it('should handle errors gracefully', async () => {
await expect(
client.generateResponse('nonexistent-model', 'test')
).rejects.toThrow();
});
});
describe('listModels', () => {
it('should return typed model information', async () => {
const models = await client.listModels();
expect(Array.isArray(models)).toBe(true);
if (models.length > 0) {
expect(models[0]).toHaveProperty('name');
expect(models[0]).toHaveProperty('size');
expect(models[0]).toHaveProperty('details');
}
});
});
});
Integration Testing
Test complete workflows with proper error handling:
// src/__tests__/conversation-service.test.ts
import { TypedOllamaClient } from '../ollama-client';
import { ConversationService } from '../conversation-service';
describe('ConversationService', () => {
let service: ConversationService;
let client: TypedOllamaClient;
beforeEach(() => {
client = new TypedOllamaClient();
service = new ConversationService(client);
});
it('should create and manage conversations', async () => {
const conversationId = service.createConversation('llama3.2:3b');
expect(conversationId).toBeDefined();
expect(typeof conversationId).toBe('string');
const conversation = service.getConversation(conversationId);
expect(conversation).toBeDefined();
expect(conversation?.model).toBe('llama3.2:3b');
});
it('should handle conversation messages with proper typing', async () => {
const conversationId = service.createConversation('llama3.2:3b');
const response = await service.sendMessage(
conversationId,
'What is TypeScript?'
);
expect(response).toBeDefined();
expect(response.role).toBe('assistant');
expect(response.content).toBeDefined();
expect(response.timestamp).toBeInstanceOf(Date);
});
});
Performance Optimization and Best Practices
Memory Management
Implement efficient memory usage patterns:
// src/memory-manager.ts
export class MemoryManager {
private static instance: MemoryManager;
private conversations: Map<string, any> = new Map();
private readonly maxConversations = 100;
private readonly maxMessageHistory = 50;
static getInstance(): MemoryManager {
if (!MemoryManager.instance) {
MemoryManager.instance = new MemoryManager();
}
return MemoryManager.instance;
}
addConversation(id: string, conversation: any): void {
// Remove oldest conversation if limit exceeded
if (this.conversations.size >= this.maxConversations) {
const oldestId = this.conversations.keys().next().value;
this.conversations.delete(oldestId);
}
// Trim message history if too long
if (conversation.messages.length > this.maxMessageHistory) {
conversation.messages = conversation.messages.slice(-this.maxMessageHistory);
}
this.conversations.set(id, conversation);
}
getConversation(id: string): any {
return this.conversations.get(id);
}
removeConversation(id: string): void {
this.conversations.delete(id);
}
getMemoryUsage(): { conversations: number; totalMessages: number } {
let totalMessages = 0;
for (const conversation of this.conversations.values()) {
totalMessages += conversation.messages.length;
}
return {
conversations: this.conversations.size,
totalMessages
};
}
}
Caching and Performance
Implement smart caching for better performance:
// src/cache-manager.ts
export class CacheManager {
private cache: Map<string, { data: any; timestamp: number; ttl: number }> = new Map();
private readonly defaultTTL = 5 * 60 * 1000; // 5 minutes
set(key: string, data: any, ttl: number = this.defaultTTL): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl
});
}
get(key: string): any | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
return entry.data;
}
clear(): void {
this.cache.clear();
}
cleanup(): void {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
}
}
}
}
Deployment and Production Considerations
Docker Configuration
Create a production-ready Docker setup:
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY dist ./dist
COPY src ./src
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S ollama -u 1001
USER ollama
EXPOSE 3000
CMD ["node", "dist/app.js"]
Environment Configuration
Set up proper environment management:
// src/config.ts
export interface AppConfig {
ollama: {
host: string;
timeout: number;
retries: number;
};
app: {
port: number;
logLevel: string;
environment: string;
};
cache: {
ttl: number;
maxSize: number;
};
}
export function loadConfig(): AppConfig {
return {
ollama: {
host: process.env.OLLAMA_HOST || 'http://localhost:11434',
timeout: parseInt(process.env.OLLAMA_TIMEOUT || '30000'),
retries: parseInt(process.env.OLLAMA_RETRIES || '3')
},
app: {
port: parseInt(process.env.PORT || '3000'),
logLevel: process.env.LOG_LEVEL || 'info',
environment: process.env.NODE_ENV || 'development'
},
cache: {
ttl: parseInt(process.env.CACHE_TTL || '300000'),
maxSize: parseInt(process.env.CACHE_MAX_SIZE || '1000')
}
};
}
Monitoring and Logging
Structured Logging
Implement comprehensive logging for production monitoring:
// src/logger.ts
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
}
export interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: any;
error?: Error;
}
export class Logger {
constructor(private level: LogLevel = LogLevel.INFO) {}
debug(message: string, context?: any): void {
this.log(LogLevel.DEBUG, message, context);
}
info(message: string, context?: any): void {
this.log(LogLevel.INFO, message, context);
}
warn(message: string, context?: any): void {
this.log(LogLevel.WARN, message, context);
}
error(message: string, error?: Error, context?: any): void {
this.log(LogLevel.ERROR, message, context, error);
}
private log(level: LogLevel, message: string, context?: any, error?: Error): void {
if (level < this.level) return;
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message,
context,
error
};
console.log(JSON.stringify(entry, null, 2));
}
}
Conclusion
TypeScript integration with Ollama creates a powerful foundation for building reliable, maintainable AI applications. The type safety, error handling, and architectural patterns demonstrated in this guide ensure your applications can handle real-world production scenarios.
Key benefits of this approach include:
- Type Safety: Catch errors at compile time rather than runtime
- Better Developer Experience: IDE autocompletion and refactoring support
- Maintainable Code: Clear interfaces and structured architecture
- Production Ready: Comprehensive error handling and monitoring
- Performance Optimized: Memory management and caching strategies
The TypeScript Ollama integration patterns shown here scale from simple scripts to complex enterprise applications. By following these practices, you'll build AI applications that are robust, maintainable, and ready for production deployment.
Start implementing these patterns in your next AI project and experience the confidence that comes with type-safe, well-structured code. Your future self—and your team—will thank you for the investment in proper TypeScript architecture.
Ready to build your next AI application with TypeScript and Ollama? The tools and patterns in this guide provide everything needed to create production-quality applications that leverage local AI models effectively.