20 Aug 2024
7 min

Parsing and mapping API response using zod.js

In this article I would like to describe the approach solving some common problems related to data fetching in Angular. Let’s consider two scenarios where an API response might present challenges for an Angular application:

  • Missing mandatory field or incorrect data type:

Even if an API documentation specifies certain fields as required, the API might occasionally return responses where these fields are missing or set to undefined. For example, suppose the API is expected to return user data with an email field as mandatory. If the API response is { “name”: “John Doe” } without the email field, your Angular application might encounter errors or behave unexpectedly when trying to access this missing field. The application logic needs to handle such cases gracefully, possibly by checking for the existence of the field before accessing it.

  • Complex or non-optimized data models:

The data model returned by the API might be overly complex or include many fields and nested objects that are not needed by your application. This can complicate the process of working with the data and require additional code to map it to a simpler structure. For instance, if the API response is deeply nested, such as { “user”: { “details”: { “profile”: { “contacts”: { “email”: “john.doe@example.com” } } } } }, and your Angular application only needs the email field, you will need to traverse multiple levels of nesting to extract it. This adds extra code and increases the potential for errors if the API structure changes.

In both cases, it’s crucial to implement robust data handling and transformation logic in your application to ensure smooth operation even when the API response deviates from expectations or is more complex than necessary. Let me introduce the approach which minimizes those risks and inconveniences and gives you full control over the type of API response. Additionally it will allow you to safely map that type into your optimized interface. Moreover, I am going to introduce zod.js library and show you the real use case of schema parsing and type inferring. 

I would like to present the implementation of angular service which is in charge of fetching the data from API, parsing the response according to a given schema and mapping it into an optimized interface which can be easily used in the template or business logic of your application. Additionally some potential errors will be catched and handled as soon as possible.

Data service

Below you can find the example of angular service which is responsible for fetching and returning response in expected shape. Deliberately the name contains a “data” phrase to indicate its single responsibility.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { catchError, map, Observable, of } from 'rxjs';
import { User } from '../users.model';
import { parseDTO } from './users.dto';
import { fromDTO } from './users.mapper';


@Injectable({
  providedIn: 'root',
})
export class UsersDataService {
  httpClient = inject(HttpClient);


  fetchUsers(): Observable<User[]> {
    const url = 'https://dummyjson.com/users';
    return this.httpClient.get(url).pipe(
      map((response) => {
        const dto = parseDTO(response);
        if (dto.success) {
          return fromDTO(dto.data);
        } else {
          console.error(dto.error);
          return [];
        }
      }),
      catchError((error) => {
        console.error(error);
        return of([]);
      })
    );
  }
}

When it receives a response, it’s trying to parse it. In case of success, which means that all required properties are in place and have expected types, it can be mapped into an app optimized model. Parsed response is called “DTO” which means “data transfer object”, it’s our contract with the backend.

In case of error, which happens when the response doesn’t meet all mandatory conditions (it’s simply different that you expect), you can handle it as you need. In the above example the service returns an empty array but it can return any value like undefined or some error message. It can be whatever is needed to properly handle the error.

Data transfer object

Now let’s have a look at the “users.dto.ts” file which contains the core part of this architecture which are schema, parse functions and inferred type.

import { z } from 'zod';


const usersSchema = z.object({
  users: z.array(
    z.object({
      id: z.number(),
      firstName: z.string(),
      lastName: z.string(),
      age: z.number().optional(),
      gender: z.string(),
      address: z.object({
        address: z.string(),
        city: z.string(),
        state: z.string(),
      }),
      company: z.object({
        address: z.object({
          address: z.string(),
          city: z.string().optional(),
          state: z.string(),
        }),
        name: z.string(),
      }),
    })
  ),
});


export type UsersDto = z.infer<typeof usersSchema>;


export function parseDTO(source: unknown) {
  return usersSchema.safeParse(source);
}

First of all, let’s mention the zod.js library which has been used here. What is zod.js? According to its documentation it is a TypeScript-first schema declaration and validation library. Long story short, it gives the developer three game changing features:

  • The possibility to define schemas. It supports primitives, complex nested objects, optionals/nullables, discriminated unions and much more. It allows you to describe every possible json object – in our case the api response.
  • Type inference. It can extract the TypeScript type of a given schema so there is no need to create a DTO type manually. Zod.js will do it for us.
  • Schema parsing. Zod will validate a given object in regard to all schema’s conditions and map it into DTO type in case of success, otherwise it will return human readable error with the details of what is wrong with the source.

In the given example “userSchema” reflects the response from the dummy api “’https://dummyjson.com/users‘” which, as you probably noticed, contains a lot of properties and not all of them are needed in our application. Therefore, only the necessary ones are included in the schema. All relevant types like string, object and array are used. If you need to describe more complex schemas, you should refer to zod.js documentation (https://zod.dev). 

Next, zod’s infer utility type is used to create a type from the schema. To check the power of this feature you can hover over UserDto type and you will notice that it properly inferred all properties, even those optional. That’s a real game changer. 

It’s worth mentioning that strict mode in tsconfig.json is required. Without it, typescript would infer every property as optional. I truly encourage you to always enable this option in your project which allows you to use the full power of TypeScript. 

Mapper

Finally, let’s describe the last but not least part of the given architecture, that is  user.mapper.ts which is responsible for mapping from DTO into an app optimized interface. 

By “optimized” I mean the object which can be easily displayed in the template or processed in business logic of the application. It should be tailor made and simple. Therefore it should be as flat as possible and contain self explanatory namings of properties with relevant types. In this example User interface looks like: 

export interface User {
  id: number;
  fullName: string;
  age?: number;
  gender: string;
  company: {
    name: string;
    address: string;
  };
  address: string;
}

Mapper is a pure function which takes “UsersDto” and returns an array of “User”. Because the response was successfully parsed, there is no need to check a particular property’s presence or type. Zod.js gives 100% guarantee that everything meets schema definition. 

import { join } from 'lodash';
import { User } from '../users.model';
import { UsersDto } from './users.dto';


export function fromDTO(dto: UsersDto): User[] {
  return dto.users.map((user) => {
    const companyAddress = user.company.address;
    const userAddress = user.address;
    const fullName = `${user.firstName} ${user.lastName}`;
    return {
      id: user.id,
      fullName,
      age: user.age,
      gender: user.gender,
      company: {
        name: user.company.name,
        address: join(
          [companyAddress.address, companyAddress.city, companyAddress.state],
          ', '
        ),
      },
      address: join(
        [userAddress.address, userAddress.city, userAddress.state],
        ', '
      ),
    };
  });
}

Now the list of the users can be easily used in the component. The response was properly parsed and mapped into an optimized model.

Wait, is it really needed?

I assume you wonder why I just didn’t add a generic type to the get method from HttpClient service. At first glance, it would return the response in an expected type so this whole fuss with schema parsing is not needed – well, nothing could be more wrong. You have no guarantee that the real type will be the same as your type. Let me prove it. 

Let’s modify our service to use some generic type.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { catchError, tap, Observable, of } from 'rxjs';
import { User } from '../users.model';


@Injectable({
  providedIn: 'root',
})
export class UsersDataService {
  httpClient = inject(HttpClient);


  fetchUsers(): Observable<User[]> {
    const url = 'https://dummyjson.com/users';
    return this.httpClient.get<User[]>(url).pipe(
      tap((users) => console.log(users)),
      catchError((error) => {
        console.error(error);
        return of([]);
      })
    );
  }
}

Although we know that api returns a different model, there is no error displayed. Angular compiler takes for granted that the response is an array of “User”. Surprisingly, when you run the code you could see a type completely different from expected. 

Obviously it will result in some error or bug in a very unexpected part of the application. That’s why the response should be parsed and the potential errors should be handled as soon as possible. With the use of architecture presented in this article, it happens just after receiving data from the backend.

Conclusion

Having these three parts (DTO, service and mapper) you can easily use data service in your component to fetch data and get it in expected shape. As you could notice this approach solves any problems connected with api response that may occur and will bring your architecture to the next level with the following enhancements:

  • It’s a guard in case of unexpected contract desynchronization between api and app. 
  • The response is validated so you are sure that all required properties are in place and have relevant types
  • Model is optimized to the app’s needs, all names are understandable and object can be easily displayed 

Repository: https://github.com/maciejkoch/angular-data-service-zod

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.