DEV Community

Sachit
Sachit

Posted on • Edited on

Typescript: Zod - Handle external api errors gracefully

In our previous post, we discussed a very cool library called Zod and how we can use it to write user defined type guards. In this post, lets look at another use case / issue this library helps resolve easily.

Lets look at the problem first.

  • You have an app that consumes data from external api's for example google maps or weather api.

  • You are working in a micro services environment and consume api's from other teams services that are loosely bound with contracts.

  • You apps have background processes ( async event handlers ) that require particular data to be passed in for it to process the event.

Lets look at this problem in code: We want to get weather for a given user

interface User {
  id: number;
  name: string;
  address?: string;
}

interface Weather {
  summary: string;
  max: number;
  min: number;
  chanceOfRain: number;
}

interface UserWeatherResponse {
  user: User;
  weather: Weather;
}

interface ResponseFromGoogleApi {
  summary: string;
  max: number;
  min: number;
  chanceOfRain: number;
  humidity: number;
}

function getUserService() {
  return {
    getUserById: (id: User["id"]) => ({
      id,
      name: "John Doe",
      email: "john@doe.com",
    }),
  };
}

function getWeatherFromGoogleApi(address: string): Weather {
  const responseFromGoogle: ResponseFromGoogleApi = {
    summary: "It will be a nice sunny day",
    max: 20,
    min: 20,
    chanceOfRain: 0,
    humidity: 10,
  };

  return {
    summary: responseFromGoogle.summary,
    max: responseFromGoogle.max,
    min: responseFromGoogle.min,
    chanceOfRain: responseFromGoogle.chanceOfRain,
  };
}

function getWeatherService() {
  return {
    getWeatherForUser: (user: User) =>
      getWeatherFromGoogleApi(user.address ?? ""),
  };
}

const userService = getUserService();
const weatherService = getWeatherService();

export const getUserWeather = (
  params: { id: number },
): UserWeatherResponse => {
  const user = userService.getUserById(params.id);
  const weather = weatherService.getWeatherForUser(user);

  return {
    user,
    weather,
  };
};


Enter fullscreen mode Exit fullscreen mode

So basically, the code above is simulating getting weather for a user from google api, that is an external dependency that our application needs in order to resolve weather requests from our users.

So what are the potential pitfalls:

  • Google api is an external dependency that we have no control over.
  • Google can choose to change its api response payload at any time.
  • Response from google api is an unknown in our application.
  • We have made assumptions as to what the response from google api will look like.
  • Google api's can have downtime and affect runtime of our application.

So while our application is typesafe at compile time, it is not immune to run time errors. For example, if goolge api does not return the required data, our application will still compile, but will break when the user is trying to access it from the browser ( runtime failure ).

Now this is where Zod excels. It can safely parse data from external api's and add runtime type safety to our application and we can gracefully handle those issue without affection the user experience. Lets take a look at how this can be done using zod:

  • We define our schema ( interfaces ) using zod.
  • We derive the type from the schema we defined.
  • We use zod to ensure the shape of the data we get from the external api is the same as what we expect in our application.
import z from "zod";

interface User {
  id: number;
  name: string;
  address?: string;
}

const weatherSchema = z.object({
  summary: z.string(),
  max: z.number(),
  min: z.number(),
  chanceOfRain: z.number(),
});

type Weather = z.infer<typeof weatherSchema>;

interface UserWeatherResponse {
  user: User;
  weather: Weather;
}

interface UserWeatherResponseError {
  message: string;
}

const responseFromGoogleApiSchema = z.object({
  summary: z.string(),
  max: z.number(),
  min: z.number(),
  chanceOfRain: z.number(),
  humidity: z.number(),
});

type ResponseFromGoogleApi = z.infer<typeof responseFromGoogleApiSchema>

function getUserService() {
  return {
    getUserById: (id: User["id"]) => ({
      id,
      name: "John Doe",
      email: "john@doe.com",
    }),
  };
}

function getWeatherFromGoogleApi(address: string): Weather | Error {
  const responseFromGoogle: ResponseFromGoogleApi = {
    summary: "It will be a nice sunny day",
    max: 20,
    min: 20,
    chanceOfRain: 0,
    humidity: 10,
  };

  if (!responseFromGoogleApiSchema.safeParse(responseFromGoogle).success) {
    // gracefull handler error here and return appropriate response to the user
    return new Error("Response from google is not what we expected, lets bail out here quickly")
  }

  return {
    summary: responseFromGoogle.summary,
    max: responseFromGoogle.max,
    min: responseFromGoogle.min,
    chanceOfRain: responseFromGoogle.chanceOfRain,
  };
}

function getWeatherService() {
  return {
    getWeatherForUser: (user: User) =>
      getWeatherFromGoogleApi(user.address ?? ""),
  };
}

const userService = getUserService();
const weatherService = getWeatherService();

export const getUserWeather = (
  params: { id: number },
): UserWeatherResponse | UserWeatherResponseError => {
  const user = userService.getUserById(params.id);
  const weather = weatherService.getWeatherForUser(user);

  if (!weatherSchema.safeParse(weather).success) {
    return {
      message: "Cannot find weather for the user"
    }
  }

  return {
    user,
    weather,
  };
};
Enter fullscreen mode Exit fullscreen mode

In summary, zod helps us easily add runtime type safety to our application. It allows us to ensure what we expect is what we get and if we don't, we can gracefully handle those errors ( no matter the reason ) and improve our application uptime and user experience.

2023, the year of Zod.

Top comments (0)