---
# System prepended metadata

title: Vue 3 - TS-REST & ZOD(EN)
tags: [Web Development, Backend Integration, Vue3, Typescript, Frontend Development, ts-rest, API Validation, zod, VueJS]

---

---
title: 'Vue 3 - TS-REST & ZOD(EN)'
disqus: hackmd
---
# Vue 3 - TS-REST & ZOD (EN)
Understanding the Benefits of API Field Validation Contracts.

When developing frontend applications in collaboration with backend teams, we often encounter a common issue:

:::info
API field types or field names are incorrectly specified, and the backend insists that it's a frontend issue. Alternatively, the frontend payload might be incorrect, but the frontend believes the backend secretly changed the API.
:::

![image](https://hackmd.io/_uploads/rJu-xTBq0.png)


In such cases, it can lead to spending a lot of time tracing the root cause of the problem, debugging, and communicating. **==However, these issues can be effectively avoided and resolved through the use of API field validation contracts.==**

We will introduce **==how to use the ts-rest and zod==** libraries in a TypeScript environment to create a **==robust API field validation contract==**, ensuring consistency in data transmission between the frontend and backend. This approach not only reduces friction during the development process but also enhances the overall stability and maintainability of the project. It’s especially beneficial when the frontend and backend need to develop in parallel according to a predefined schema under tight deadlines.


## ts-rest
https://ts-rest.com/
A tool for defining and typing APIs that allows the frontend and backend to share the same API definition contract, enabling developers to catch type inconsistencies in API fields at compile time.

## zod
https://zod.dev/
A powerful type-checking and data validation tool that can be used to define and validate object data structures, seamlessly integrating with ts-rest.

By leveraging these two tools, we can establish a clear and reliable API auto-validation process, ensuring data integrity and correctness. The following example demonstrates how to implement these features in a Vue 3 project.

Let's install the packages:
`npm install @ts-rest/core zod`


## Designing a Basic API Contract

Assume our API includes an OTP password GET request required during login and a POST request for the product list.

We will name them `OtpResponseSchema` and `GoodsListResponseSchema`, respectively.

```typescript=
// src/service/api.ts

import { request } from '@/service/request' // axios request instance
import { initContract } from '@ts-rest/core' // ts-rest initialization method
import type { AppRoute, ClientInferRequest } from '@ts-rest/core' // Request route and other type definitions
import { z } from 'zod' // Main methods from ZOD

/** Define the response data structure for two APIs */
const OtpResponseSchema = z.object({
  Code: z.number(),
  Msg: z.string(),
  Data: z.object({
    OTP: z.string()
  })
})

// The reason for the initial capital letters is simply because
// my API is designed this way. Modify it according to your own project needs.
const GoodsListResponseSchema = z.object({
  Code: z.number(),
  Msg: z.string(),
  Data: z.array(
    z.object({
      Show: z.boolean(),
      Price: z.number(),
      CreatedAt: z.number(),
      DeletedAt: z.number().nullable(),
      Description: z.string(),
      GoodsSpecs: z.array(
        z.object({
          ID: z.number(),
          GoodsID: z.number(),
          Specs: z.string(),
          CreatedAt: z.number()
        })
      ),
      ImageUrls: z.array(
        z.object({
          ID: z.number(),
          Name: z.string(),
          Alias: z.string(),
          DeletedAt: z.number().nullable(),
          CreatedAt: z.number()
        })
      )
    })
  )
})

/** API Contract for validation */

// The `satisfies` operator is a new feature introduced in TypeScript 4.9.
// Its main purpose is to perform type checks while retaining the precision of literal types.

const getOtpResponse = {
  method: 'GET',
  path: '/admin/otp',
  responses: {
    200: OtpResponseSchema
  },
  summary: 'Login'
} satisfies AppRoute

const getGoodsListResponse = {
  method: 'POST',
  path: '/member/goods/list',
  responses: {
    200: GoodsListResponseSchema
  },
  summary: 'Get goods list',
  body: z.object({
    page: z.number(),
    pageSize: z.number()
  })
} satisfies AppRoute

export const apiContract = initContract().router(
  {
    getOtpResponse,
    getGoodsListResponse
  },
  {
    pathPrefix: '/api'
  }
)

// Generate interface, which can be directly used for type checking in components
export interface ApiContract extends ClientInferRequest<typeof apiContract> {}

```

**src/views/Home.vue** (or any other component that needs to request APIs)

```typescript=
// src/views/Home.vue
// script setup

import { ref, onMounted, nextTick } from 'vue'
import { getOtp, getGoodsList } from '@/service/api'
import { apiContract } from '@/service/api'

// Directly use the checker we defined in Zod as an interface
type GoodsItem = z.infer<typeof GoodsListResponseSchema>['Data'][number]
const goodsList = ref<GoodsItem[]>([])

const getOtpNumbers = async () => {
  try {
    const res = await getOtp({
      method: apiContract.getOtpResponse.method,
      url: apiContract.getOtpResponse.path
    })
    const validation = apiContract.getOtpResponse.responses[200]
    if (res.data.Code === 200 && validation.parse(res.data)) {
      state.value.currentOtp = res.data.Data.OTP
    }
  } catch (error: any) {
    throw new Error(error.toString())
  }
}

const fetchGoodsList = async (page: number, pageLimit: number) => {
  try {
    const res = await getGoodsList({
      method: apiContract.getGoodsListResponse.method,
      url: apiContract.getGoodsListResponse.path,
      data: {
        Page: page,
        PageLimit: pageLimit
      }
    })

    const validation = apiContract.getGoodsListResponse.responses[200]
    if (res.data.Code === 200 && validation.parse(res.data)) {
      goodsList.value = res.data.Data

    }
  } catch (error: any) {
    // Handle error
    console.error('Failed to fetch goods list:', error)
    throw new Error(error.toString())
  }
}

onMounted(async () => {
  await nextTick()
  await getOtpNumbers()
  await fetchGoodsList(1, 20)
})
</script>
```

If Zod detects any type mismatches during the API request, you will see this error in the console:
![image](https://hackmd.io/_uploads/ry1DVaHqA.png)

This means that the OTP field was **expected to be a number**, but the **returned value was a string.**

At this point, you can check the API documentation to see who made the mistake with the field type… 😃😃😃

## Conclusion
When the development timeline feels tighter than your favorite pair of jeans after a big meal 🍕, having a reliable API contract can be a lifesaver.

By treating the API documentation as the ultimate truth, both frontend and backend teams can work in harmony —even when those inevitable UI challenges or backend quirks pop up. 🍻🍻🍻

While we can't promise this will eliminate all the hiccups (because, let's face it, what’s development without a little drama? 😅), it sure does help in catching those pesky data format issues early on. 

This not only cuts down on endless back-and-forths and debugging marathons 🏃‍♂️, but also boosts overall development efficiency. In the end, your application will be as robust as a superhero in a blockbuster movie.
🚀🚀🚀

###### tags: `VueJS` `Vue3` `Typescript` `API Validation` `ts-rest` `zod` `Frontend Development` `Backend Integration` `Web Development`