TS-Rest Client

ts-rest also allows for optionally consuming the contracts on client side. This gives utmost type-safety, auto complete, and more data validation(on the client side). The biggest aspect here is that you are not repeating yourself.

The Users client

We can use the initClient function from ts-rest/core to construct a client for our contracts. Here is a short example to construct the Users client from the User contract.

import { initClient } from '@ts-rest/core';
 
/*Assume UserContract is imported from a shared lib, or a package(or a monolith)*/
 
const baseUrl = 'http://localhost:4000';
const baseHeaders = {};
const Users = initClient(UserContract, {
        baseUrl,
        baseHeaders
});

Login and registration flow

The following is a short example for a scenario where you want to register a user, and then logging them in. We have also done appropriate error handling based on the status codes.

const name = "Jane";
const email = "Jane@doe.com";
const password = "123456789";
 
const res = await Users.register({ body : { name, email, password }});
 
if(res.status != 201 && res.status != 400) throw res;
 
/* The user has either been created now, or they already exist */
const user = await Users.login({ body : { email, password }});
 
if(user.status !== 200) throw user;
 
const token = user.body.token;

Intelligent typing

The really cool thing here is that since the responses returned by the client are typed as discriminated unions, we can use the state attribute to get intelligent auto complete, and type-safety. For example, the user login should either return 200 with user details and a token, or 400 with an error message according to our contract. Since there is a check to throw an error if the user status is not 200, after that line of code, typescript will intelligently deduce that the type of the user body can only be the user attributes, and with intellisense you get a really nice auto-complete. Feel free to try this in VScode.

Post creation and fetching flow

Creation

Now lets create a post using the Posts client.

const token = user.body.token;
 
const Posts = initClient(PostContract, {
  baseUrl,
  baseHeaders : {
      authorization : `BEARER ${token}`
  }
});
 
const post = await Posts.createPost({ 
    body : { 
			title : "My First Post",
			content : "First post made through ts-rest client"
		}
});
 
if(post.status != 201) throw post;

Notice how we have passed the authorization header here in the baseHeaders of the client object. Commonly we pass the token value as BEARER <token>. This header will be passed to each endpoint in the contract.

Fetching

In the same way, we can use the Posts client to fetch posts as well.

const postId = String(post.body.id);
 
const samePost = await Posts.getPost({
    params : { id : postId },
});
 
if(samePost.status != 200) throw samePost;

Important Insight

The thing worth appreciating here is that ts-rest does not enforce us to couple our client side, and server side like this. We have this option available if we need it. So you can either use it like a regular REST API, or you can optionally choose to have a more RPC like behavior.