In today's interconnected applications, integrating with external APIs is a common requirement. However, without proper type safety, these integrations can quickly become a source of runtime errors and maintenance headaches. TypeScript offers a powerful solution to these challenges by providing compile-time type checking. In this article, we'll explore how to build robust, type-safe API integrations using TypeScript, with practical examples using the Vedika Astrology API.
The Problem with Untyped API Integrations
When working with APIs in JavaScript, we often find ourselves making assumptions about the shape of data we'll receive. This leads to several issues:
- Runtime errors when the API response doesn't match our expectations
- Poor IDE autocompletion and IntelliSense
- Difficult refactoring when API structures change
- Increased maintenance burden
TypeScript solves these problems by allowing us to define interfaces for our API data, catching type mismatches at compile time rather than runtime.
Step 1: Setting Up Our Project
First, let's initialize a new Node.js project with TypeScript:
mkdir vedika-api-integration
cd vedika-api-integration
npm init -y
npm install typescript @types/node axios ts-node nodemon --save-dev
npx tsc --init
Now, let's configure our tsconfig.json with the following settings:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
Step 2: Defining Type Interfaces
Before making API calls, let's define interfaces for the Vedika API request and response. According to the Vedika documentation, we need to send a question and birth details, and we'll receive astrology insights.
Create a file src/types.ts:
export interface BirthDetails {
datetime: string; // ISO 8601 datetime string
latitude: number;
longitude: number;
}
export interface VedikaRequest {
question: string;
birthDetails: BirthDetails;
}
export interface VedikaResponse {
insights: string;
generatedAt: string;
confidence: number;
relatedTopics?: string[];
}
Step 3: Creating a Type-Safe API Client
Now, let's create a client for the Vedika API with proper error handling and type safety.
Create src/vedikaClient.ts:
import axios, { AxiosError, AxiosResponse } from 'axios';
import { VedikaRequest, VedikaResponse } from './types';
const API_BASE_URL = 'https://api.vedika.io/v1/astrology/query';
export class VedikaClient {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async getAstrologyInsights(request: VedikaRequest): Promise<VedikaResponse> {
try {
const response: AxiosResponse<VedikaResponse> = await axios.post(API_BASE_URL, request, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
const axiosError = error as AxiosError;
// Handle different types of errors
if (axiosError.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
throw new Error(`API Error: ${axiosError.response.status} - ${axiosError.response.data}`);
} else if (axiosError.request) {
// The request was made but no response was received
throw new Error('No response received from API');
} else {
// Something happened in setting up the request that triggered an Error
throw new Error(`Request error: ${axiosError.message}`);
}
}
}
}
Step 4: Using the Client in Our Application
Now let's create an example that uses our type-safe client.
Create src/index.ts:
import { VedikaClient } from './vedikaClient';
import { VedikaRequest, BirthDetails } from './types';
async function main() {
// Initialize the client with your API key
const client = new VedikaClient('your-api-key-here');
// Prepare the request with proper typing
const request: VedikaRequest = {
question: "What does my future hold in my career?",
birthDetails: {
datetime: "1990-05-15T08:30:00Z",
latitude: 40.7128,
longitude: -74.0060
}
};
try {
// Get astrology insights with full type safety
const response = await client.getAstrologyInsights(request);
console.log('Astrology Insights:');
console.log(`Generated at: ${response.generatedAt}`);
console.log(`Confidence: ${response.confidence * 100}%`);
console.log(`Insights: ${response.insights}`);
if (response.relatedTopics && response.relatedTopics.length > 0) {
console.log('\nRelated Topics:');
response.relatedTopics.forEach(topic => console.log(`- ${topic}`));
}
} catch (error) {
console.error('Error:', error.message);
}
}
main();
Practical Tips and Gotchas
- Environment Variables: Never hardcode API keys in your code. Use environment variables:
import dotenv from 'dotenv';
dotenv.config();
const client = new VedikaClient(process.env.VEDIKA_API_KEY || '');
- Partial Response Types: Sometimes APIs return different structures based on conditions. Use union types:
type ApiResponse = SuccessResponse | ErrorResponse;
interface SuccessResponse {
status: 'success';
data: VedikaResponse;
}
interface ErrorResponse {
status: 'error';
error: string;
code: number;
}
- Request Validation: Consider using a library like Zod to validate request data:
import { z } from 'zod';
const birthDetailsSchema = z.object({
datetime: z.string().datetime(),
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
});
const requestSchema = z.object({
question: z.string().min(1),
birthDetails: birthDetailsSchema,
});
function validateRequest(request: unknown): VedikaRequest {
return requestSchema.parse(request);
}
- Pagination Handling: For APIs that return paginated data, create a helper function:
async function getAllPages<T>(fetchPage: (page: number) => Promise<{ data: T[]; hasNextPage: boolean }>): Promise<T[]> {
const allData: T[] = [];
let page = 1;
let hasNextPage = true;
while (hasNextPage) {
const response = await fetchPage(page);
allData.push(...response.data);
hasNextPage = response.hasNextPage;
page++;
}
return allData;
}
Conclusion
Building type-safe API integrations with TypeScript significantly improves the reliability and maintainability of your applications. By defining clear interfaces, handling errors properly, and leveraging TypeScript's type system, we can catch issues early and write more robust code.
With the Vedika API example, we've seen how to create a client that provides full type safety for both requests and responses. The principles demonstrated here can be applied to any API integration.
Next Steps
- Explore advanced TypeScript features like conditional types and mapped types for more complex API scenarios
- Consider implementing a caching layer for API responses
- Add rate limiting to handle API quotas
- Create automated tests for your API client using libraries like Jest
- Explore API mocking tools like MSW for development without hitting real endpoints
By following these practices, you'll be well on your way to building professional-grade API integrations that are both type-safe and maintainable.
Top comments (0)