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:
- Install json-server :
yarn add json-server
- Next create a db.json in your project
{
"data": [
{
"title": "Todo 1",
"id": 1
},
{
"title": "todo 2",
"id": 2
}
]
}
- 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 thisz.object({ id: z.number() }).required()
. (If you define your path like this :/:idPost
your pathParams must bez.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
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
Top comments (0)