From Tedious to Simple: Reshaping Your API Integration Experience with Zodios
Tags: typescript, react, api, zodios
Published: 2023-03-20 04:13:00

---
title: From Tedious to Simple: Reshaping Your API Integration Experience with Zodios
published: true
description: Currently, APIs have become an essential component of modern software development. However, due to their complex integration experience, it is not always an easy process for developers. To simplify this process, Zodios offers a solution that can help you free yourself from the complexities of API integration, making it easier for you to develop applications. In this article, we will explore how to use Zodios to reshape your API integration experience and provide you with a simpler and more efficient development solution.
tags: Typescript, React, API, Zodios
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gzvyh08vn6mv1jrr9cvd.png
published_at: 2023-03-20 12:13 +0800
---

Currently, APIs have become an essential component of modern software development. However, due to their complex integration experience, it is not always an easy process for developers. To simplify this process, `Zodios` offers a solution that can help you free yourself from the complexities of API integration, making it easier for you to develop applications. In this article, we will explore how to use `Zodios` to reshape your API integration experience and provide you with a simpler and more efficient development solution.

> The Chinese version simultaneously published on Medium: [從繁到簡：使用 Zodios 重塑您的 API 串接體驗](https://medium.com/漸強實驗室-crescendo-lab-engineering-blog/從繁到簡-使用-zodios-重塑您的-api-串接體驗-f3fa9faf59e3)

## Technical Background

The technical foundation for multiple large-scale products in "[Crescendo Lab](https://www.cresclab.com/)" mainly adopts Typescript and React. Typescript helps us write more stable and maintainable code, while React is a popular JavaScript front-end framework that allows us to quickly build highly interactive web applications.

Our web application is presented in the form of a Single Page Application (SPA), providing a smooth user experience. Meanwhile, we utilize RESTful APIs for data exchange to better manage and control data transmission. It is worth mentioning that due to frequent updates to our company's product features, we often need to repeatedly verify APIs and add new features. Therefore, the integration work related to APIs is very burdensome for us.

The following, we will list several pain points or common issues that we have encountered during development in this environment, and present our perspectives, analyze the problems, and search for solutions.

## Incorrect Use of `useEffect`

Here's a classic example of misusing `useEffect`:

```tsx
useEffect(() => {
  async function fetchData() {
    const response = await fetch(`https://example.com/api/orgs/${orgId}`);
    const json = await response.json();
    setData(json);
  }
  fetchData();
}, [orgId]);
```

This approach may trigger an error warning when unmounting, and may also result in a race condition when switching `orgId`. Some may even consider disabling the eslint rule `react-hooks/exhaustive-deps` to take control of when the `useEffect` behavior is triggered, but these actions are extremely dangerous and can disrupt the developer experience.

The correct approach is to use an abort controller signal or a cancel flag. There is plenty of information about this available online, and it is not the focus of this article, so we will not go into detail here.

In addition, this example also includes improper URL composition, which will be further discussed below.

## Poor URL composition method

The parameters that may appear in a URL include path parameters and search parameters. The most common mistake is the incorrect concatenation of these parameters, such as in the following example:

```tsx
const url = `users/${userId}/tags?q=${name}`;
```

The concatenation method above may cause errors due to the presence of certain characters, therefore it is necessary to use `urlencode`:

```tsx
const url = `users/${strictUriEncode(userId)}/tags?q=${strictUriEncode(name)}`;
```

However, this concatenation method is really ugly. Especially in the part of search parameters, it also lacks flexibility. Therefore, we prefer to use `path-to-regexp` together with `query-string` or `qs` library:

```tsx
const url = pathToRegexp.compile('users/:userId/tags')({ userId }) + '?' + queryString.stringify({ q: name });
```

In this way, we have successfully extracted parameters from the URL. However, we still have another challenge to solve because our API uses the `snack_case` naming convention, while the front-end coding style uses `camelCase`.

## `snake_case` vs `camelCase`

Due to the different naming conventions, the frontend needs to convert both the search parameter keys and the property keys inside the request body to `snake_case` before making a request. After receiving the response, the property keys inside the data need to be converted to `camelCase`. This process is tedious and repetitive. Ideally, the conversion should be done at a lower level and should not be a concern during usage.

## Conversion Issues with `Date` Type

Due to the inability to transmit `Date` objects in APIs, we typically use serialized data with common formats such as `ISO-8601` / `RFC3339` (sending numbers is considered a hack). Each time we obtain a value in a component, it is in string format and requires additional conversion when used. If we could consistently obtain Date objects from the start, the user experience when working with the data would be much better. In addition to clearly knowing that the value is a Date (otherwise it is a string), we could also directly use `Date` methods. If using `Day.js`, using `Day.js` objects directly would also be a good choice.

## Misunderstandings Caused by Human Language

This is a true story that happened in our company. We designed a new specification for a specific resource. At the time, the agreement between the backend engineer and the frontend was that the old version would not have a certain property, only the new version would. Without this record, I (as the frontend engineer) used a very aggressive judgment method, `hasOwnProperty`. However, the backend engineer only returned the property value as `null`. This resulted in a judgment error because I passed the old version data to the new version processing method, ultimately causing a program error and interruption.

Another interesting example often occurs in the communication between engineers and designers, which is the definition of "required". From an API perspective, "required" means that the field must exist, but from a user's perspective, it means "non-empty".

Such information disparity is easy to occur in human communication interfaces with simple descriptions. However, if we can use tools such as schemas or documents to help clarify and even provide playground testing, we can truly achieve consensus through a unified language.

In the case of inconsistent type definitions, errors often occur during program execution, making it difficult to trace the problem. Therefore, a better approach is to perform preliminary data validation after receiving the API response. From a team perspective, this allows the team to determine whether the problem should be handled by the frontend or backend engineer as soon as possible. Some people may think that in most cases, skipping validation can still allow the program to run normally. However, this optimistic approach ultimately makes the program unreliable. However, for methods such as `GraphQL` or `tRPC` that can generate frontend and backend interfaces simultaneously through schema, additional data validation is not necessary.

## Scattered APIs

Mixing API implementation with the app makes maintenance difficult. For instance, if we make adjustments to the API, it will be challenging to locate the affected code within a large project. Therefore, a more ideal approach is to place the API definitions in a separate package, so that once there is any change to the API specifications, we only need to make corresponding adjustments in the API package.

## Cache Key Chaos

The most common challenge for beginners when using `TanStack Query` or `SWR` is managing the cache and cache keys. To ensure that updates are reflected in the interface, we usually invalidate the specified query after the request to resynchronize data with the backend. Managing this in a component can be very cumbersome and prone to omissions during development or maintenance. Complex cache management, like memory management, can be quite challenging. While we do pay attention to memory usage when writing JavaScript programs, we don't actually issue commands to control it, do we? Establishing a logic or strategy to automate the management of cache and cache keys, and even automatically updating data, can greatly reduce the development burden. The simplest and most extreme approach is to invalidate all queries after every mutate, and `tRPC` provides such a strategy: **[https://trpc.io/docs/useContext#invalidate-full-cache-on-every-mutation](https://trpc.io/docs/useContext#invalidate-full-cache-on-every-mutation)**

## Encounter with `Zodios`

In order to solve the aforementioned issues, we established some standards to evaluate tools during our search:

- To avoid the cognitive burden caused by using `useEffect`, it is necessary to be able to integrate `TanStack Query` or other similar tools.
- It must be possible to define `Date` and convert `ISO8601` in the response to `Date` type.
- Definitions must be simple and clear.
- It would be even better if type guards can be provided.

Recently, I discovered that `Zod` released the `z.coerce` feature in `v3.20`, particularly the `z.coerce.date()` function which can both validate and convert date formats returned from APIs. As a result, I began searching for solutions in the `Zod` ecosystem and found `Zodios`.

`Zodios` is a convenient tool that allows you to implement code as documentation by simply describing the API specification. It uses `zod` to achieve end-to-end type safety. Additionally, thanks to its integration with `axios` and `Tanstack Query`, it delivers excellent results in terms of extensibility, development experience, and even user experience. Here is a simple example that illustrates how to use `Zodios` from defining the API specification to applying it in a component:

```tsx
import { makeApi, Zodios } from '@zodios/core';
import { ZodiosHooks } from '@zodios/react';
import { z } from 'zod';

const api = makeApi([
  {
    alias: 'getUsers',
    method: 'get',
    path: 'orgs/:orgId/users',
    response: z.array(
      z.object({
        id: z.string().uuid(),
        name: z.string(),
        email: z.string().email(),
        createdAt: z.coerce.date(),
      }),
    ),
  },
]);

const apiClient = new Zodios('/api', api);
const apiHooks = new ZodiosHooks('myAPI', apiClient);

const query = apiHooks.useGetUsers({
  params: {
    orgId,
  },
});
```

Prior to this, we have adjusted the structure of the `pnpm workspace`. We created a new package in this repository specifically for defining APIs. In this project, we created a file called `models.ts` to define the schema of all models, which can be thought of as the concept of Open API's Models. At the same time, we try to make the models reusable. For some special cases, we handle them separately inline in the endpoint. For example, if a specific API lacks a status field in the response, we will simply omit it, like this:

```tsx
// frontend-repo/packages/api-sdk/user.ts

{
  alias: 'update',
  method: 'put',
  path: 'api/v1/orgs/:orgId/users/:userId',
  parameters: [
    {
      name: 'body',
      type: 'Body',
      schema: CreateUpdateRequestBodySchema,
    },
  ],
  response: UserSchema.omit({
    status: true,
  }),
},
```

We have divided the API functions into multiple independent modules, each of which is a separate file and uses its own `makeAPI()`. In addition, we have made some adjustments to `@zodios/react`, so that each module can automatically invalidate queries of the same module. We can also establish relationships between modules so that they can influence each other, thereby avoiding the trouble of managing cache and cache keys, and not invalidating all queries on the screen every time there is a mutate.

```
/frontend-repo
└── packages
    ├── api-sdk
    │   ├── api
    │   │   ├── message.ts
    │   │   ├── organization.ts
    │   │   ├── team.ts
    │   │   └── user.ts
    │   └── models.ts
    └── app
        ├── src
        ├── vite.config.ts
        ├── package.json
        └── tsconfig.json
```

It is worth noting that the `CreateUpdateRequestBodySchema` in the above example will be kept in the file of the module instead of being placed in the globally shared `models.ts`. This helps to avoid the `models.ts` becoming too bulky due to excessive repetitive content used only in specific areas.

As `Zodios` integrates with `axios`, we take advantage of this feature to convert the request body and query string from `camelCase` to `snake_case` on `axios`, as well as converting the response from `snake_case` to `camelCase`. We also handle token management and `baseURL` on `axios`. Additionally, `useMutation().reset()` itself does not abort requests, so even if the status is reset, the `onSuccess`, `onError`, and `onSettled` callbacks will still be triggered when the response returns. Therefore, we have created a simple shared function that can be inserted into the object returned by `useMutation`, which can reset the mutation and truly cancel the request at the same time.

We will send the final result to an object called `cantata` (also the development code name for this product's server). From now on, we only need to write the API specification in this package, and we can directly use all API requests through this object. Once the specification changes, we can immediately understand the affected range and make corresponding adjustments through typescript's type checking.

```tsx
const userListQuery = cantata.user.useList({ orgId });
const currentUserQuery = cantata.user.userGetById({ orgId, userId });

const updateCurrentUserMutation = cantata.user.useUpdate({ orgId, userId });
// Note that both `userListQuery` and `currentUserQuery` will be automatically invalidated after each mutation.
```

## API Guidelines

`list` or `getById` queries should use the `GET` method, while mutations such as `create` / `update` / `delete` / `enable` / `disable` should use `POST`, `PUT`, `PATCH`, `DELETE` methods. For some queries, such as `search`, using `POST` instead of `GET` may be necessary. In this case, adding `immutable: true` to the API definition will change the API from a mutation to a query.

To make the use of API more concise and clear, we recommend using aliases instead of `useQuery` and `useMutation` when calling the API. This can avoid the impact of cumbersome path and method on the application. However, we do not want these aliases to overwrite the original zodios hooks. Therefore, we will try to avoid using names such as `get` and `delete` as aliases, and instead use alternative solutions such as `getById` and `deleteById`.

```tsx
// ✅ Preferred approach
api.useList();

// ❌ Avoid using this approach
api.useQuery('/list');
```

## Further Benefits and Future Planning

Creating API SDK with `Zodios` is very easy to maintain, even for backend engineers who are not familiar with TypeScript. With type checking, we can even wrap `makeApi` to make the checks more strict. By centralizing these files in a package, it also makes maintaining API standards more convenient, without being interfered by irrelevant content. `Zodios` can also directly output OpenAPI documents, which can be browsed through tools like Swagger UI. Moreover, since the API contract is in the repository, it means that it can be committed and even submitted for Pull Request code review like any other code, and it can even generate a changelog.

Currently, `Zodios` provides integration with `React` and `Solid`, as well as `Express`. Our next plan is to establish a stub server through `@zodios/express`, and maintain test cases in it. We will also provide an interface for each API to switch between test cases or bypass directly to the server maintained by the backend, making it easy for front-end or test engineers to switch between different states when developing or viewing corresponding screens, and reduce the dependency of the front-end on the development efficiency and shared environment of the backend.

## Conclusion

We firmly believe that separating the development of API SDK and application development is the way to improve the maintenance of APIs in front-end code. By delegating repetitive and tedious tasks to the underlying layer for unified processing, the pain of using APIs directly in the application will be greatly reduced. Using API SDK can greatly reduce work costs and increase work efficiency. In addition, since maintaining API SDK only requires focusing on API contracts themselves, it is also very easy. Extracting the maintenance of APIs from the application is like “*turning one difficult task into two simple tasks”* for developers. Through the application verification of `Zodios`, we have obtained a good result. Therefore, we are sharing the problems and ideas we have seen in the past here, hoping to provide help to everyone.

## Reference

- [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
- [axios](https://github.com/axios/axios)
- [graphql](https://graphql.org/)
- [OpenAPI](https://www.openapis.org/)
- [pnpm](https://pnpm.io/)
- [Swagger UI](https://swagger.io/tools/swagger-ui/)
- [swr](https://swr.vercel.app/)
- [TanStack Query](https://tanstack.com/query)
- [trpc](https://trpc.io/)
- [Zod](https://zod.dev/)
- [Zodios](https://www.zodios.org/)

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bc0sygmrttg2rgml4vhp.png" alt="This article was assisted by ChatGPT and the image was also generated through Stable Diffusion">
  <figcaption>This article was assisted by ChatGPT and the image was also generated through Stable Diffusion</figcaption>
</figure>