Building Robust Applications in React with TypeScript and Zod for REST API Validation

Marcos Schead - May 23 - - Dev Community

When building applications with React and TypeScript, leveraging TypeScript's static typing capabilities can significantly enhance your code's reliability. However, even with TypeScript, you can't guarantee the shape and type of data coming from external APIs. This potential discrepancy can lead to runtime errors that disrupt your application's functionality. In this blog post, we'll explore how to handle such situations using Zod, a TypeScript-first schema declaration and validation library.

Scenario: Fetching User Data with TypeScript

Imagine you have a React application that fetches user data from an API. The User object has a phoneNumber property, which is expected to be a string. You'll format this phone number to display it in a user-friendly manner. Here's how you might start:

// types.ts
export interface User {
  id: number;
  name: string;
  phoneNumber: string;
}

// api.ts
export const fetchUser = async (userId: number): Promise<User> => {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const data: User = await response.json();
  return data;
};

// UserComponent.tsx
import React, { useEffect, useState } from 'react';
import { User } from './types';
import { fetchUser } from './api';

const UserComponent: React.FC<{ userId: number }> = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .catch(err => setError(err.message));
  }, [userId]);

  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>Loading...</div>;

  const formattedPhoneNumber = formatPhoneNumber(user.phoneNumber);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Phone: {formattedPhoneNumber}</p>
    </div>
  );
};

const formatPhoneNumber = (phoneNumber: string): string => {
  // Logic to format the phone number
  return phoneNumber.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
};

export default UserComponent;
Enter fullscreen mode Exit fullscreen mode

In this example, we expect phoneNumber to be a string. However, if the backend changes phoneNumber to a number, this will lead to a runtime error in the formatPhoneNumber function.

Runtime Error Example

Suppose the backend now sends:

{
  "id": 1,
  "name": "John Doe",
  "phoneNumber": 1234567890
}
Enter fullscreen mode Exit fullscreen mode

This change will cause a runtime error in the formatPhoneNumber function because replace is not a method on numbers. The error will look like this:

TypeError: phoneNumber.replace is not a function
Enter fullscreen mode Exit fullscreen mode

This error indicates that phoneNumber is not a string as expected, but a number, leading to the failure of the replace method.

Introducing Zod for Schema Validation

To safeguard against such issues, we can use Zod to validate the data we receive from the API. Here's how we can do it step-by-step:

  1. Install Zod :
npm install zod
Enter fullscreen mode Exit fullscreen mode
  1. Define a Zod Schema :
// schema.ts
import { z } from 'zod';

export const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  phoneNumber: z.string(),
});

export type User = z.infer<typeof userSchema>;
Enter fullscreen mode Exit fullscreen mode
  1. Validate API Response and Provide Feedback :
// api.ts
import { userSchema, User } from './schema';

export const fetchUser = async (userId: number): Promise<User> => {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const data = await response.json();

  const result = userSchema.safeParse(data);

  if (!result.success) {
    console.error("Validation Error:", result.error.format());
    throw new Error('Invalid data format');
  }

  return result.data;
};
Enter fullscreen mode Exit fullscreen mode
  1. Handle Validation in the Component :
// UserComponent.tsx
import React, { useEffect, useState } from 'react';
import { User } from './schema';
import { fetchUser } from './api';

const UserComponent: React.FC<{ userId: number }> = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .catch(err => setError(err.message));
  }, [userId]);

  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>Loading...</div>;

  const formattedPhoneNumber = formatPhoneNumber(user.phoneNumber);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Phone: {formattedPhoneNumber}</p>
    </div>
  );
};

const formatPhoneNumber = (phoneNumber: string): string => {
  // Logic to format the phone number
  return phoneNumber.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
};

export default UserComponent;
Enter fullscreen mode Exit fullscreen mode

In this updated example, the fetchUser function uses the Zod schema to validate the API response. If the data doesn't match the expected schema, an error is thrown, and a detailed validation error is logged to the console. This feedback helps developers quickly identify and resolve issues with the API data.

Error Message Example

With the Zod validation in place, if the backend sends the phoneNumber as a number, the application will catch this discrepancy and throw an error. The console will log a detailed error message like this:

Validation Error: {
  "phoneNumber": {
    "_errors": ["Expected string, received number"]
  }
}
Enter fullscreen mode Exit fullscreen mode

In addition to logging the error, the application will throw an error to the user with the message:

Error: Invalid data format
Enter fullscreen mode Exit fullscreen mode

This message indicates to the user that there was an issue with the data received from the API.

Conclusion

Using Zod for schema validation in your React and TypeScript applications ensures that your application can gracefully handle unexpected changes in the API response structure. This approach helps you catch potential issues early, leading to more robust and reliable applications. By incorporating Zod into your development workflow, you can enhance your application's resilience against API changes and reduce runtime errors.

Remember, while TypeScript provides excellent compile-time type checking, runtime validation with tools like Zod is essential for dealing with external data sources. This combination ensures that your application remains stable and predictable, even when the data it relies on changes unexpectedly.

. . . . . .