DEV Community

Vedika Intelligence
Vedika Intelligence

Posted on

Building Type-Safe API Integrations with TypeScript: A Practical Guide with Vedika Astrology API [2026-02]

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:

  1. Runtime errors when the API response doesn't match our expectations
  2. Poor IDE autocompletion and IntelliSense
  3. Difficult refactoring when API structures change
  4. 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
Enter fullscreen mode Exit fullscreen mode

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/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

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[];
}
Enter fullscreen mode Exit fullscreen mode

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}`);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Practical Tips and Gotchas

  1. 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 || '');
Enter fullscreen mode Exit fullscreen mode
  1. 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;
}
Enter fullscreen mode Exit fullscreen mode
  1. 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);
}
Enter fullscreen mode Exit fullscreen mode
  1. 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;
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Explore advanced TypeScript features like conditional types and mapped types for more complex API scenarios
  2. Consider implementing a caching layer for API responses
  3. Add rate limiting to handle API quotas
  4. Create automated tests for your API client using libraries like Jest
  5. 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)