From Flaky to Flawless: Angular API Response Management with Zod
Tags: angular, typescript, webdev, javascript
Published: 2024-04-25 14:43:45

As a front-end developer, this has surely happened to you before. You start your computer in the morning, launch your Angular project, open [http://localhost:4200](http://localhost:4200/) and are greeted by several runtime errors that weren't there the previous day. Your date pipe returns an 'Invalid Date' string and some components don't even render anymore. After your unit tests are still green and you haven't changed any other code since the last workday, there's only one culprit left: the backend. To save you work in the future and find faulty backend responses that were not agreed with you, I will show you a simple, but powerful solution with zod in this blog post.

# **Zod 101: An Introduction to Schema Validation in TypeScript**

[Zod](https://zod.dev/) is an open-source schema declaration and validation library that emphasizes TypeScript. It can refer to any data type, from simple to complex. Zod eliminates duplicative type declarations by inferring static TypeScript types and allows easy composition of complex data structures from simpler ones. It has no dependencies, is compatible with Node.js and modern browsers, and has a concise, chainable interface. Zod is lightweight (8kb when zipped), immutable, with methods returning new instances. It encourages parsing over validation and is not limited to TypeScript but works well with JavaScript as well.

Let's examine a simple example of how Zod helps us define and validate schemas.

```tsx
import { z } from "zod";

// creating a schema for strings
const mySchema = z.string();

// parsing
mySchema.parse("tuna"); // => "tuna"
mySchema.parse(12); // => throws ZodError

// "safe" parsing (doesn't throw error if validation fails)
mySchema.safeParse("tuna"); // => { success: true; data: "tuna" }
mySchema.safeParse(12); // => { success: false; error: ZodError }
```

By using the safeParse function, we can return responses from the backend to our components while displaying any validation errors.

# **Defining API Response Models with Zod**

Now, let's examine a schema that defines the response model from the backend. For this example, I'll use the [JSONPlaceholder API](https://jsonplaceholder.typicode.com/).

```tsx
import { z } from 'zod';

export const CommentSchema = z.object({
  postId: z.number(),
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  body: z.string(),
});

export type Comment = z.infer<typeof CommentSchema>;
```

As illustrated, I've defined the Zod schema as an exported constant, which can be used later in a function that's yet to be defined. Additionally, I've exported a type using the infer function, allowing it to be used in your Angular code without referencing the Zod library.

# **How to Validate API Responses Using Zod in Angular**

After creating our zod schema and exporting the type based on that schema, we can construct a helper function to validate our API responses. Given that the Angular HttpClient returns Observables, it's logical to create a custom rxjs operator function.

```tsx
import { isDevMode } from '@angular/core';
import { map, pipe } from 'rxjs';
import { z } from 'zod';

export function verifyResponse<T extends z.ZodTypeAny>(zodObj: T) {
  return pipe(
    map((response) => {
      if (isDevMode()) {
        const result = zodObj.safeParse(response);

        if (!result.success) {
          console.error(result.error);
        }
      }
      return response as z.infer<T>;
    })
  );
}
```

The `verifyResponse` function takes a Zod object as an argument. It uses the `pipe` function from RxJS to create a new function that maps responses to a validation process.

If the Angular application is in development mode (`isDevMode()`), it attempts to parse the response using Zod's `safeParse` method. If parsing is unsuccessful, it logs the error to the console. Although you could use the parse function to validate the response model, this would throw an error that needs to be caught. To make identifying incorrect response models simpler, I recommend using the error handling as demonstrated.

Finally, regardless of whether it's in development mode or not, it returns the unmodified response. This allows any components or services using this helper function to behave as if it wasn't there when not in development mode.

# **Integrating Schema Definitions and Validation in Angular Services**

We can now bring everything together and define our Angular service. This service will call our backend and validate our response model. Thanks to Zod, the response is automatically typed based on the given schema. However, it's always good practice to explicitly define the return type.

```tsx
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { CommentSchema, Comment } from '../models/comment.model';
import { verifyResponse } from '../utils/schema.validator';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class CommentsService {
  private readonly http = inject(HttpClient);

  public getCommentById(id: number): Observable<Comment> {
    return this.http
      .get<Comment>('https://jsonplaceholder.typicode.com/comments/' + id)
      .pipe(verifyResponse(CommentSchema));
  }
}
```

To illustrate the error output, I modified the schema to assume incorrect property types.

```bash
schema.validator.ts:12 ZodError: [
  {
    "code": "invalid_type",
    "expected": "number",
    "received": "string",
    "path": [
      "body"
    ],
    "message": "Expected number, received string"
  }
]
```

# Conclusion

In conclusion, utilizing Zod for API response validation can significantly improve the robustness of your Angular application. It allows you to catch and handle unexpected or incorrect backend responses before they cause runtime errors in your application. By integrating Zod with Angular, you can ensure that your frontend codebase is more resilient and less prone to bugs due to faulty backend responses. Remember, it's always better to prevent errors than to debug them!