Handling API responses with Advance Typescript

Handling API responses with Advance Typescript
APIs handling using TypeScript at Node for various chains

Writing simple TypeScript is easy and fun, but when it comes to advanced TypeScript, it becomes complex and difficult. One of the most challenging use cases that engineering teams encounter is integrating backend APIs with their frontend applications. This requires handling errors, common payloads, user validations, and retrieving lengthy data formats, making it difficult to cover the right set of types for API responses. As a result, engineering teams may resort to using the “any” type declaration, leading to the loss of the enormous value of type checking.

In this blog, the team at Node finance will aim to simplify this process and make type checking enjoyable during API integrations.

Node finance provides a tool suite for Web2/Web3 companies to easily onboard to the blockchain ecosystem without needing to understand the underlying complex technology. One of our core products is a CRM dashboard, which allows our B2B customers to analyse their user data on a web app. This is what it looks like.

Node Finance CRM Dashboard

This blog is written in three steps to improve the types on a single codebase:

i) Beginner type declarations.

ii) Intermediate type declarations

iii) Advanced type declarations.

To provide examples, we will take Node Finance CRM Dashboard APIs, which provide us with production-level API implementations, and write type declarations on them..

i) Beginner type declarations: Any API can consist of common payloads like pagination data, errors, etc. and at times different payloads that contain data for the API call. Within APIResponseData, we have used type unions, extending interfaces, and the “in” operator for dictionary definitions.

<!-- common payload: stats associated with data -->
type StatsRange = {
  max: number, min: number
}

type StatsKeys = 'count' | 'aum' | 'priceusd' | 'value'

type PartialStats = {
  stats?: {
    [key in StatsKeys]?: StatsRange
  }
}

<!-- common payload: pagination info -->
type Pagination = {
  pagination?: {
    currentPage: number,
    perPage: number,
    total: number,
    totalPages: number
  }
}

<!-- api: onboarding data -->
type APIResponseOnboards = {
  count: number,
  unixTimeStamp: number,
  timestamp: string,
}

<!-- api: historical data -->
type APIResponseHistorical = {
  aum: number,
  unixTimeStamp: number,
}

<!-- api: wallets data -->
type APIResponseWallets = {
  wid: number,
  tenant: number,
  portfolioValue: number,
  address: string,
}

<!-- all api data handling -->
interface APIResponseData extends PartialStats, Pagination {
  data : {
    data: APIResponseOnboards[] | APIResponseHistorical[],
    field?: string,
    grouping?: string
  }[] | APIResponseWallets[],
}

beginner-typescript-api-response.ts

ii) Intermediate type declarations: The above code is somewhat hard-coded and it will make “APIResponseData” more and more complex as more APIs are added. To handle all the data-consuming APIs, we will use Generics and other Utility Types.

<!-- common payload: stats associated with data -->
type StatsRange = {
  max: number, min: number
}

type StatsKeys = 'count' | 'aum' | 'priceusd' | 'value'

interface PartialStats<Type extends string|number|symbol, RangeType> {
  stats?: Partial<Record<Type, RangeType>>
}

<!-- common payload: pagination info -->
type Pagination = {
  pagination?: {
    currentPage: number,
    perPage: number,
    total: number,
    totalPages: number
  }
}

<!-- api: onboarding data -->
type APIResponseOnboards = {
  count: number,
  unixTimeStamp: number,
  timestamp: string,
}

<!-- api: historical data -->
type APIResponseHistorical = {
  aum: number,
  unixTimeStamp: number,
}

<!-- api: wallets data -->
type APIResponseWallets = {
  wid: number,
  tenant: number,
  portfolioValue: number,
  address: string,
}

<!-- all api data handling -->
interface APIResponseData<Type> extends PartialStats<StatsKeys, StatsRange>, Pagination {
  data : {
    data: Type[],
    field?: string,
    grouping?: string
  }[] | APIResponseWallets[],
}

intermediate-typescript-api-response.ts

iii) Advanced type declarations: To approach a more robust and modular solution, we will need to categorize APIs and define various versions of “APIResponseData -> APIResponses” for different types of data, such as authentication/authorization data. In such scenarios, Conditional Types come in very handy along with all the previously used strategies.

<!-- common payload: stats associated with data -->
type StatsRange = {
  max: number, min: number
}

type StatsKeys = 'count' | 'aum' | 'priceusd' | 'value'

type PartialStats<
  T extends Partial<{
    [key in StatsKeys]: any;
  }> = {
    [key in StatsKeys]: any;
  },
> = {
  stats?: {
    [key in keyof T]?: StatsRange;
  };
};

<!-- common payload: pagination info -->
type Pagination = {
  pagination?: {
    currentPage: number,
    perPage: number,
    total: number,
    totalPages: number
  }
}

<!-- api: onboarding data -->
type APIResponseOnboards = {
  count: number,
  unixTimeStamp: number,
  timestamp: string,
}

<!-- api: historical data -->
type APIResponseHistorical = {
  aum: number,
  unixTimeStamp: number,
}

<!-- api: wallets data -->
type APIResponseWallets = {
  wid: number,
  tenant: number,
  portfolioValue: number,
  address: string,
}

<!-- all api data handling -->
type GroupedResponse<T> = (
  { data: T[]} | { data: T[]; field: string; grouping: string}
)[]

type DataServiceResponse<
  BasePayload extends Record<string, any>,
  DataShape extends BasePayload | Array<BasePayload> = BasePayload,
> = {
  data: DataShape;
} & (DataShape extends Array<any> ? Pagination : null) &
  (BasePayload extends Partial<{ [k in StatsKeys]: number }>
    ? PartialStats<BasePayload>
    : null);

advanced-typescript-api-response.ts

By handling APIs in this way, we were able to make our frontend technology much more robust and fully enable TypeScript end-to-end, helping us build faster and more scalable tech systems. For more engineering blogs like this, follow the Node finance medium publication.

Do check out other blogs on Invariant Violation with Token Swaps on Celo. I hope you will love them.