Typesafe API with TS-Rest, React Query
Tags: typescript, react, nextjs, javascript
Published: 2023-12-26 06:47:13

Hi, today I'll introduce you to a library called ts-rest. A library that supports API organization while integrating with the typesafe API.

## What is TS-Rest ?

In short, TS-Rest is a library help you define your API and give you an end to end safe API.

In addition, TS-Rest can help you define API normally (you can define Put, Patch, Delete, Get, Post Method).

If you work with a Third Party API. TS-Rest is a good choice to leverages TypeScript's type system to provide strong typing for API endpoints, request/response payloads, and data models. This ensures that you're working with the correct data types and minimizes the risk of runtime errors due to incorrect data handling.

## How to use TS-Rest ?

In this part, I will be creating a simple Todo app to introduce TS-Rest on the client side.

In this project, I will use CRA for initilizing my React app.
These are libraries I will use in this project : 

- React Query 
- @ts-rest/core for creating contract,router
- @ts-rest/react-query 

So let's get started 🚀

**Installation**

Before we're going to project please make sure you have installed @ts-rest library and react-query library.

`yarn add @ts-rest/react-query @tanstack/react-query @ts-rest/core`

**Setup fake API server**

In this project I will use json-server for creating fake API server. 

Steps: 

1. Install json-server : `yarn add json-server`
2. Next create a db.json in your project

```
{
  "data": [
    {
      "title": "Todo 1",
      "id": 1
    },
    {
      "title": "todo 2",
      "id": 2
    }
  ]
}
```
3. In `package.json` add this script:

```
scripts : {
  .....
  "server": "json-server --watch db.json --port 5000"
  .....
}
```
So let's run `yarn server`. This will start out server in this project.

**Setup ts-rest router**

First create `src/constants/index.ts` for defining some API Route and Server URL. 

```
src/constants/index.ts

export const SERVER_URL = 'http://localhost:5000/data';

export const API_ROUTE = {
  GET: '/',
  CREATE: '/',
  UPDATE: '/:id',
  DELETE: '/:id',
};
```

Next, create `lib/ts-rest` folder in your src directory. Inside that we will create two file :

```
//lib/ts-rest/router.ts

import { initContract } from '@ts-rest/core';
import { API_ROUTE } from '../../constants';
import { z } from 'zod';

const c = initContract();

export const router = c.router({
  getAllTodo: {
    method: 'GET',
    path: API_ROUTE.GET,
    responses: {
      200: z.array(
        z.object({
          id: z.number(),
          title: z.string(),
        }),
      ),
    },
  },
  createTodo: {
    method: 'POST',
    path: API_ROUTE.CREATE,
    responses: {
      201: z.object({
        id: z.number(),
        title: z.string(),
      }),
    },
    body: z.object({
      title: z.string(),
    }),
  },
  updateTodo: {
    method: 'PATCH',
    path: API_ROUTE.UPDATE,
    responses: {
      204: z.object({
        id: z.number(),
        title: z.string(),
      }),
    },
    pathParams: z.object({ id: z.number() }).required(),
    body: z.object({
      title: z.string(),
    }),
  },
  deleteTodo: {
    method: 'DELETE',
    path: API_ROUTE.DELETE,
    responses: {
      200: z.object({
        id: z.number(),
        title: z.string(),
      }),
    },
    pathParams: z.object({ id: z.number() }).required(),
    body: z.undefined(),
  },
});
```

This code is simply the define route of API.
For each property in router we have : 

- method :  Definition of that API's method
- path : Definition of that API's path
- reponses : Definition of that API's return data type (for example : in getAllTodo's response when fetch data success. It's will return an array have type `{id:number;title:string}[]`. 
- pathParams : Definition of that API's path param type. If that API Route uses path params, you must define this. For example in `deleteTodo`'s path I have declared the path is `/:id`. So I need create a pathParams like this `z.object({ id: z.number() }).required()`. (If you define your path like this : `/:idPost` your pathParams must be `z.object({ idPost: z.number() }).required()`
- body : Definition of that API's body type.

Finally in `lib/ts-rest/client-provider` ,let create a clientProvider to query and mutate data from server. 

```
import { initQueryClient } from '@ts-rest/react-query';
import { router } from './router';
import { SERVER_URL } from '../../constants/';

export const clientProvider = initQueryClient(router, {
  baseUrl: SERVER_URL,
  baseHeaders: {},
});
```
If you want custom your api (override). You can use `api` property in initQueryClient's options. Please refer [here](https://ts-rest.com/docs/core/custom)

So that's all. We have configured our TS-Rest client provider completely.

**Setup react-query and create some component**

First in `App.tsx` let's setup `react-query` provider.

```
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from 'react-hot-toast';

const queryClient = new QueryClient()

export const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <div className='flex flex-col items-center py-10'>
        <InputTodo />
        <TodoList />
        <Toaster />
      </div>
    </QueryClientProvider>
  );
};
```
Let's create `TodoList,InputTodo` component

```
src/components/TodoList.tsx

import { clientProvider } from "../lib/ts-rest/client-provider"
import TodoItem from "./TodoItem"

const TodoList = () => {
  const { data, isLoading, } = clientProvider.getAllTodo.useQuery(['GET_ALL_TODO'])

  return (
    isLoading ? <p>Loading...</p> :
      <div className="flex flex-col">
        {data?.body.map(todoItem => <TodoItem {...todoItem} key={todoItem.id} />)}
      </div>
  )
}

export default TodoList
```

```
src/components/TodoList.tsx

import { clientProvider } from "../lib/ts-rest/client-provider"
import TodoItem from "./TodoItem"

const TodoList = () => {
  const { data, isLoading, } = clientProvider.getAllTodo.useQuery(['GET_ALL_TODO'])

  return (
    isLoading ? <p>Loading...</p> :
      <div className="flex flex-col">
        {data?.body.map(todoItem => <TodoItem {...todoItem} key={todoItem.id} />)}
      </div>
  )
}

export default TodoList
```

In `TodoList` component we have use clientProvider to query the data from server. Thanks to Ts-Rest, every time call clientProvider.getAllTodo you will ensure the Safe API, making it easier for maintaining and debugging.

```
src/components/TodoItem.tsx

const TodoItem = ({ id, title }: { id: number, title: string }) => {
  const { mutateAsync } = clientProvider.deleteTodo.useMutation();
  const queryClient = useQueryClient()

  const onDelete = async () => {
    try {
      await mutateAsync({
        params: { id }
      })
      queryClient.invalidateQueries({ queryKey: ['GET_ALL_TODO'] })
      toast.success("Delete success")
    } catch (error) {
      toast.error(JSON.stringify(error))
    }
  } 

  return (
    <div>
      <span>{title}</span>
      <div className="flex gap-3">
        <button onClick={onDelete}>
          Delete
        </button>
      </div>
    </div>
  )
}
```

Also this is an example of using useMutation with TS-Rest. It helps you control the type of pathParams input. 

Finally let's create a `InputTodo` component.

```
const InputTodo = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const { mutateAsync } = clientProvider.createTodo.useMutation()
  const queryClient = useQueryClient()

  const onAddTodo = async () => {
    try {
      if (inputRef.current) {
        await mutateAsync({
          body: {
            title: inputRef.current?.value
          }
        })
        toast.success('Add todo success');
        queryClient.invalidateQueries({ queryKey: ['GET_ALL_TODO'] })

      }
    } catch (error) {
      toast(JSON.stringify(error))
    }
  }

  return (
    <div>
      <input ref={inputRef} placeholder="Add todo..." />
      <button
        onClick={onAddTodo}>Add</button>
    </div>
  )
}

export default InputTodo
```
In addition to `pathParams` TS-Rest can also type safe for body input.

So's that all 🙏 about demo project.

## Summary

TS-Rest is great in helping you manage your API Design and easy to maintain.

However, it still has some points I am not satisfied with Ts-Rest.

- It is still a library being developed so the community is not much.
- Document has not been written clearly (this is my opinion).
- Currently, it is still not used with React-Query V5. So I hope they will update in the next time.

Thanks for reading. 👏

Repo link :  https://github.com/quangnmwork/ts-rest-tutorial