Building a typesafe Typescript REST API client

Building a typesafe Typescript REST API client

Typical data fetching examples

  1. This is the typical way of fetching data in Typescript using the Fetch API:
const response = await fetch("https://example.com/movies");
const jsonData = await response.json();

Then we need to cast the jsonData, because it's type is any:

const movies = jsonData as Movies;

  1. Some people create a generic wrapper around fetch:
async function request<TResponse>(
  url: string,
  config: RequestInit
): Promise<TResponse> {
  const response = await fetch(url, config);
  return await response.json();
}

And then fetching the data looks like this:

const movies = await request<Movies>("https://example.com/movies");

The problem

This is absolutely fine, however if your API is big enough, you're left with a giant redundant code - you need to configure the method (POST, PUT, DELETE), then stringify the body / add query parameters ect.

We can utilize some Typescript magic to help us with that.

I created a ts-rest-api-client repository which contains full documentation and an example with Node.js.

The API of this client looks like this:

// GET  /movies
const movies = await client("/movies").get();

It's type-safe, so you don't need to do any type casting! Also, the urlPath is also fully-typed:

Image

If you misstype the urlPath, you will receive Typescript error.


Path parameters are also supported:

// GET  /posts/1
await client("/movies/{id}", "1").get();

You can also use queryParameters:

// GET  /comments?postId=1
const comments = await client("/comments").get({
  queryParameters: {
    postId: 1,
  },
});

Other HTTP methods (POST, PUT, PATCH, DELETE) - are also supported:

// POST  /posts
await client("/posts").post({
  body: {
    title: "foo",
    body: "bar",
    userId: 1,
  },
});

^^^ body will be automatically serialized to JSON under the hood.


You can also extend this client with auth. By default, JWT Beareris supported.

Just change extended interface in http/requests.ts from QueryOptions (or Mutation Options to AuthQueryOptions (or AuthMutationOptions):

http/requests.ts
export interface CreatePostOptions extends AuthMutationOptions {
  body: {
    title: string;
    body: string;
    userId: number;
  };
}
// POST  /posts
await client("/posts").post({
  body: {
    title: "foo",
    body: "bar",
    userId: 1,
  },

  jwtToken: "", // Add your JWT Token here
});

Defining your endpoints schema is simple:

http/schema.ts
export interface EndpointsSchema {
  "/posts": {
    get: () => Promise<Post[]>;
    post: (options: CreatePostOptions) => Promise<Post>;
  };
  "/posts/{id}": {
    get: () => Promise<Post>;
    put: (options: PutPostOptions) => Promise<Post>;
    patch: (options: PatchPostOptions) => Promise<Post>;
    delete: () => Promise<void>;
  };
  "/comments": {
    get: (options: GetCommentsByPostIdOptions) => Promise<Comment[]>;
  };
  "/posts/{id}/comments": {
    get: () => Promise<Comment[]>;
  };
}

Full documentation is available here.


See all posts