DEV Community

Cover image for Fetch data using RxJs & validate them using Zod
Amin
Amin

Posted on

Fetch data using RxJs & validate them using Zod

Why would I want to use RxJs in the first place?

RxJs is a library for creating code that follows the principles of functional & reactive programming, a code-style that allows the developer to think more of what to expect rather than how to get a result.

This library makes it easy to reason about any data structures, whether it is synchronous (arrays, objects, ...) or asynchronous (promises, websocket, events, streams, ...).

Okay but what about Zod?

Zod is yet another library that will help us validate data. It creates a schema that we can then use against any values in order to check that these values are indeed what we expect them to be.

This makes it stupidly easy to create reliable code that might have to deal with unreliable or unstable data sources wuch as an external HTTP API for example.

For instance, If one of your colleagues updates your server and change the data that will flow through your client application, you might come into runtime errors that might be tricky to spot right away.

Zod makes it easy to create reliable and deterministic code that behaves as it should. It also makes the burden of having to deal with validation errors a breeze.

Convinced?

If you are convinced that you should be using RxJs & Zod for your next projects, let's hop on and try to fetch a list of users from the JSONPlaceholder API as an example to see how it might be done.

Create an observable for fetching the users

Create an observable that will fetch the users from the JSONPlaceholder API.

const endpoint = "https://jsonplaceholder.typicode.com/users";

const users$ = fromFetch(endpoint);
Enter fullscreen mode Exit fullscreen mode

The fromFetch function is an operator that will help us create observable values directly from a Fetch request. We simply feed it the URL that we want to fetch from, and it runs the request for us, giving us back an observable that we can listen to.

Subscribe to the observable updates

Listen for any updates, whether it is new values, errors or complete events that the observable might trigger in a near future.

users$.subscribe({
  next: users => {
    console.log(users);
  },
  error: error => {
    if (error instanceof Error) {
      console.error(error.message);
    } else {
      console.log(error);
    }
  },
  complete: () => {
    console.log("Completed");
  }
});
Enter fullscreen mode Exit fullscreen mode

You actually don't have to listen for the complete event, but it might be interesting in order to close a loader for instance in a frontend application.

Transform the Response received from the Fetch request

Transform any Response that might be sent from the server from the Fetch request and grab only the JSON body. Also handle any bad status code that might be sent from the server's response.

const endpoint = "https://jsonplaceholder.typicode.com/users";

const users$ = fromFetch(endpoint).pipe(
  switchMap((response) => {
    if (!response.ok) {
      return throwError(() => new Error("Bad response"));
    }

    return from(response.json());
  })
);
Enter fullscreen mode Exit fullscreen mode

Operators are the bread & butter of RxJs, allowing us to apply small but efficient functions into our code, leaving us with a clean & concise API, rather than having to deal with it in an imperative way.

Create a schema that will validate the response body

Create a schema using Zod in order to validate that the data we received from the response body is what we expect it to be.

const usersSchema = z.array(z.object({
  id: z.number(),
  email: z.string().email(),
  username: z.string()
}));
Enter fullscreen mode Exit fullscreen mode

Schemas in Zod allow us to reason our code in terms of data. It makes it really easy to see what the expected outcome might be even without having to know exactly the endpoint we are trying to reach.

Validate the response body

This will apply our previously defined schema against the response body.

map((response) => {
  return usersSchema.parse(response);
})
Enter fullscreen mode Exit fullscreen mode

Validating unreliable or unstable data using Zod is as easy as calling one little method. So you are one call away from getting near zero runtime errors in your application.

Wrap up

Here is the full code for reference.

import { fromFetch } from "rxjs/fetch";
import { from, map, switchMap, throwError } from "rxjs";
import { z } from "zod";

const usersSchema = z.array(z.object({
  id: z.number(),
  email: z.string().email(),
  username: z.string()
}));

const endpoint = "https://jsonplaceholder.typicode.com/users";

const users$ = fromFetch(endpoint).pipe(
  switchMap((response) => {
    if (!response.ok) {
      return throwError(() => new Error("Bad response"));
    }

    return from(response.json());
  }),
  map((response) => {
    return usersSchema.parse(response);
  })
);

users$.subscribe({
  next: users => {
    console.log(users);
  },
  error: error => {
    if (error instanceof Error) {
      console.error(error.message);
    } else {
      console.log(error);
    }
  },
  complete: () => {
    console.log("Completed");
  }
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

You know are starting to get the idea of RxJS & Zod.

Next step is to grab more informations from the official documentation for RxJs & Zod and build your next awesome and reliable application.

Note that in this example, we talked about frontend applications, but RxJs also shines in the backend with frameworks like Nest.js that can leverage Rx.js's observables out-of-the-box.

Using Zod & Validation Pipes you can receive data from frontend applications without having to manually and imperatively validating data.

RxJs & Zod are two fantastic pieces of code that you can use to leverage & scale your applications right now.

Here is an added bonus on how you might fetch the list of articles for each users for instance.

import { fromFetch } from "rxjs/fetch";
import { from, map, concatMap, switchMap, throwError } from "rxjs";
import { z } from "zod";

const usersSchema = z.array(z.object({
  id: z.number(),
  email: z.string().email(),
  username: z.string()
}));

const articlesSchema = z.array(z.object({
  id: z.number(),
  userId: z.number(),
  title: z.string(),
  body: z.string()
}));

const baseEndpoint = "https://jsonplaceholder.typicode.com";

const endpoint = `${baseEndpoint}/users`;

const users$ = fromFetch(endpoint).pipe(
  switchMap((response) => {
    if (!response.ok) {
      return throwError(() => new Error("Bad users"));
    }

    return from(response.json());
  }),
  map((response) => {
    return usersSchema.parse(response);
  })
);

const usersWithArticles$ = users$.pipe(
  switchMap((users) => {
    return from(users);
  }),
  concatMap((user) => {
    const endpoint = `${baseEndpoint}/posts?userId=${user.id}`;

    return fromFetch(endpoint).pipe(
      switchMap((response) => {
        if (!response.ok) {
          return throwError(() => new Error("Bad posts"));
        }

        return from(response.json());
      }),
      map(response => {
        return articlesSchema.parse(response);
      }),
      map(articles => {
        return {
          ...user,
          articles
        }
      })
    );
  }),
);

usersWithArticles$.subscribe({
  next: users => {
    console.log(users);
  },
  error: error => {
    if (error instanceof Error) {
      console.error(error.message);
    } else {
      console.log(error);
    }
  },
  complete: () => {
    console.log("Completed");
  }
});
Enter fullscreen mode Exit fullscreen mode

Top comments (0)