DEV Community

Cover image for From Flaky to Flawless: Angular API Response Management with Zod
Daniel Sogl
Daniel Sogl

Posted on

From Flaky to Flawless: Angular API Response Management with Zod

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 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 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.

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

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.

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

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.

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

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.

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

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

schema.validator.ts:12 ZodError: [
  {
    "code": "invalid_type",
    "expected": "number",
    "received": "string",
    "path": [
      "body"
    ],
    "message": "Expected number, received string"
  }
]
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)