Skip to content

Commit 3fa43d9

Browse files
Allow extra API client args, allow headers, and prettify query args (#179)
Co-authored-by: Youssef Gaber <1728215+Gabrola@users.noreply.github.com>
1 parent 9d27607 commit 3fa43d9

24 files changed

Lines changed: 1257 additions & 419 deletions

File tree

.changeset/calm-bananas-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/react-query': minor
3+
---
4+
5+
Allow custom API to allow extra args, type them in the react query api calls

.changeset/clean-dots-relate.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ts-rest/core': minor
3+
'@ts-rest/solid-query': minor
4+
---
5+
6+
Export the default fetch API as `tsRestFetchApi` from the core library, enables you to easy modify the fetcher

.changeset/early-pillows-train.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@ts-rest/core': minor
3+
'@ts-rest/react-query': minor
4+
'@ts-rest/solid-query': minor
5+
---
6+
7+
Add the ability to remove a baseHeader on a per-request basis by setting the headers value to undefined
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/solid-query': minor
3+
---
4+
5+
Add solid-query support for extra args and custom headers

.changeset/pretty-books-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/core': minor
3+
---
4+
5+
Allow custom API to allow extra args, and type them in the individual api calls

.changeset/spicy-kiwis-teach.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@ts-rest/core': minor
3+
'@ts-rest/react-query': minor
4+
'@ts-rest/solid-query': minor
5+
---
6+
7+
Prettify the arguments of API calls, making it much clearer what data should be passed to ts-rest api calls (thanks @mattpocockuk!)

README.md

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,60 @@
2525

2626
# Introduction
2727

28-
ts-rest provides an RPC-like client side interface over your existing REST APIs, as well as allowing you define a _separate_ contract implementation rather than going for a 'implementation is the contract' approach, which is best suited for smaller or simpler APIs.
28+
ts-rest offers a simple way to define a contract for your API, which can be both consumed and implemented by your application, giving you end to end type safety without the hassle or code generation.
2929

30-
If you have non typescript consumers, a public API, or maybe want to add type safety to your existing REST API? ts-rest is what you're looking for!
31-
32-
## Features
30+
### Features
3331

3432
- End to end type safety 🛟
35-
- Magic RPC-like API 🪄
36-
- Tiny bundle size 🌟 (1kb!)
33+
- RPC-like client side interface 📡
34+
- [Tiny bundle size 🌟](https://bundlephobia.com/package/@ts-rest/core) (1kb!)
3735
- Well-tested and production ready ✅
3836
- No Code Generation 🏃‍♀️
39-
- Zod support for body parsing 👮‍♀️
37+
- Zod support for runtime type checks 👮‍♀️
4038
- Full optional OpenAPI integration 📝
4139

40+
### Super Simple Example
41+
42+
Easily define your API contract somewhere shared
43+
44+
```typescript
45+
const contract = c.contract({
46+
getPosts: {
47+
method: 'GET',
48+
path: '/posts',
49+
query: z.object({
50+
skip: z.number(),
51+
take: z.number(),
52+
}), // <-- Zod schema
53+
responses: {
54+
200: c.response<Post[]>(), // <-- OR normal TS types
55+
},
56+
},
57+
});
58+
```
59+
60+
Fulfil the contract on your sever, with a type-safe router:
61+
62+
```typescript
63+
const router = s.router(contract, {
64+
getPost: async ({ params: { id } }) => {
65+
return {
66+
status: 200,
67+
body: prisma.post.findUnique({ where: { id } }),
68+
};
69+
},
70+
});
71+
```
72+
73+
Consume the api on the client with a RPC-like interface:
74+
75+
```typescript
76+
const result = await client.getPosts({
77+
query: { skip: 0, take: 10 },
78+
// ^-- Fully typed!
79+
});
80+
```
81+
4282
## Quickstart
4383

4484
Install the core package

apps/docs/docs/core/custom.md

Lines changed: 109 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,131 @@
11
# Custom Client
22

3-
:::info
4-
The `credentials` option has no effect when using a custom client. Make sure you handle credentials in your custom client
5-
(e.g., setting `withCredentials` in axios).
3+
By default, not specifying an `api` will use the default `tsRestFetchApi` which uses fetch under the hood.
4+
5+
```typescript
6+
const client = initQueryClient(postsApi, {
7+
baseUrl: 'http://localhost:5003',
8+
baseHeaders: {},
9+
// Uses `tsRestFetchApi` by default
10+
});
11+
```
12+
13+
## Adding a Custom API
14+
15+
If you want a custom api, you can reuse the internal `tsRestFetchApi` to add logging/custom logic to your requests!
16+
17+
```typescript
18+
const client = initQueryClient(postsApi, {
19+
baseUrl: 'http://localhost:5003',
20+
baseHeaders: {},
21+
api: async (args) => {
22+
// Add anything you want here!
23+
24+
return tsRestFetchApi(args);
25+
},
26+
});
27+
```
28+
29+
### Extra Query Arguments
30+
31+
By default when you make a ts-rest request you can pass in `params`, `query`, `body`, `headers` etc. However, sometimes you may want to pass in extra arguments to your custom api. You can do this by extending the type of the `args` parameter in your custom api.
32+
33+
```typescript
34+
const client = initQueryClient(postsApi, {
35+
baseUrl: 'http://localhost:5003',
36+
baseHeaders: {},
37+
api: async (args: ApiFetcherArgs & { myCustomArg?: string }) => {
38+
if (args.myCustomArg) {
39+
// do something with myCustomArg ✨
40+
}
41+
42+
return tsRestFetchApi(args);
43+
},
44+
});
45+
```
46+
47+
The magical bit, is this is now fully typed and will work with your IDE's autocomplete! 🤯
48+
49+
One note here, any extra args which are provided here but aren't typed correctly - e.g. if you've `@ts-expect-error`'d, **will still be passed to your api**. This is because the `args` parameter is a spread of all the other arguments you pass in to your api.
50+
51+
```typescript
52+
client.getPosts({
53+
query: { skip: 0, take: 10 },
54+
myCustomArg: 'hello',
55+
// ^-- autocomplete will work here, allowing you to extend ts-rest however you want
56+
});
57+
```
58+
59+
:::tip
60+
You can use this to accomplish loads of patterns, such as adding a `cache` argument to your api, or adding a `logger` argument to your api - maybe you want to add an `onUploadProgress` argument to your api to track upload progress? You can do all of this with the `args` parameter!
661
:::
762

863
## Using Axios (custom api override)
964

10-
By default ts-rest ships with an incredibly simple fetch
65+
By default ts-rest ships with an incredibly simple fetch
1166
implementation for data fetching, because fetch requires zero extra
12-
dependencies and works perfectly for most use cases, however,
67+
dependencies and works perfectly for most use cases, however,
1368
sometimes you may want to use Axios, or another data fetching strategy, for that
1469
you can pass a `api` attribute to the `initClient` or `initQueryClient`.
1570

71+
:::info
72+
The `credentials` option has no effect when using a custom client. Make sure you handle credentials in your custom client
73+
(e.g., setting `withCredentials` in axios).
74+
:::
75+
1676
**Here's a basic example: **
77+
1778
```typescript
18-
import { contract } from './some-contract'
19-
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios'
79+
import { contract } from './some-contract';
80+
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
2081

2182
const client = initClient(contract, {
22-
baseUrl: "http://localhost:3333/api",
83+
baseUrl: 'http://localhost:3333/api',
2384
baseHeaders: {
24-
'Content-Type': 'application/json'
85+
'Content-Type': 'application/json',
2586
},
2687
api: async ({ path, method, headers, body }) => {
27-
const baseUrl = 'http://localhost:3333/api' //baseUrl is not available as a param, yet
88+
const baseUrl = 'http://localhost:3333/api'; //baseUrl is not available as a param, yet
2889
try {
2990
const result = await axios.request({
3091
method: method as Method,
3192
url: `${this.baseUrl}/${path}`,
3293
headers,
3394
data: body,
34-
})
35-
return { status: result.status, body: result.data }
95+
});
96+
return { status: result.status, body: result.data };
3697
} catch (e: Error | AxiosError | any) {
3798
if (isAxiosError(e)) {
38-
const error = e as AxiosError
39-
const response = error.response as AxiosResponse
40-
return { status: response.status, body: response.data }
99+
const error = e as AxiosError;
100+
const response = error.response as AxiosResponse;
101+
return { status: response.status, body: response.data };
41102
}
42-
throw e
103+
throw e;
43104
}
44105
},
45-
})
106+
});
46107
```
47108

48109
Sometimes you need dynamic headers, IE passing in a Bearer token. There are two approaches you can take:
49110

50111
### Instantiate the `client` with the header passed in:
51112

52113
```typescript
53-
import { contract } from './some-contract'
54-
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios'
114+
import { contract } from './some-contract';
115+
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
55116

56117
export class SampleAPI {
57-
token: string
118+
token: string;
58119
constructor(params: { token: string }) {
59-
this.token = params.token
60-
this.baseUrl = 'http://localhost:3333/api'
120+
this.token = params.token;
121+
this.baseUrl = 'http://localhost:3333/api';
61122
}
62123
client = () => {
63124
return initClient(contract, {
64125
baseUrl: this.baseUrl,
65126
baseHeaders: {
66127
Authorization: `Bearer ${idToken}`,
67-
'Content-Type': 'application/json'
128+
'Content-Type': 'application/json',
68129
},
69130
api: async ({ path, method, headers, body }) => {
70131
try {
@@ -73,33 +134,33 @@ export class SampleAPI {
73134
url: `${this.baseUrl}/${path}`,
74135
headers,
75136
data: body,
76-
})
77-
return { status: result.status, body: result.data }
137+
});
138+
return { status: result.status, body: result.data };
78139
} catch (e: Error | AxiosError | any) {
79140
if (isAxiosError(e)) {
80-
const error = e as AxiosError
81-
const response = error.response as AxiosResponse
82-
return { status: response.status, body: response.data }
141+
const error = e as AxiosError;
142+
const response = error.response as AxiosResponse;
143+
return { status: response.status, body: response.data };
83144
}
84-
throw e
145+
throw e;
85146
}
86147
},
87-
})
88-
}
148+
});
149+
};
89150
}
90151
```
91152

92153
### Instantiate the `client` but access a token during runtime:
93154

94-
Here's an example using the `firebase/auth` library. Because `api` is async, you can `await` various calls when using the method.
155+
Here's an example using the `firebase/auth` library. Because `api` is async, you can `await` various calls when using the method.
95156

96157
```typescript
97-
import { contract } from './some-contract'
98-
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios'
158+
import { contract } from './some-contract';
159+
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
99160
export class SampleAPI {
100-
authInstance: Auth
161+
authInstance: Auth;
101162
constructor(params: { authInstance: Auth }) {
102-
this.authInstance = params.authInstance
163+
this.authInstance = params.authInstance;
103164
}
104165
client = () => {
105166
return initClient(contract, {
@@ -108,30 +169,30 @@ export class SampleAPI {
108169
'Content-Type': 'application/json',
109170
},
110171
api: async ({ path, method, headers, body }) => {
111-
const token = await this.authInstance.currentUser.getIdToken()
172+
const token = await this.authInstance.currentUser.getIdToken();
112173
try {
113174
const result = await axios.request({
114175
method: method as Method,
115176
url: `${this.baseUrl}/${path}`,
116-
headers: {
177+
headers: {
117178
...headers,
118-
Authorization: `Bearer ${idToken}`
179+
Authorization: `Bearer ${idToken}`,
119180
},
120181
data: body,
121-
})
122-
return { status: result.status, body: result.data }
182+
});
183+
return { status: result.status, body: result.data };
123184
} catch (e: Error | AxiosError | any) {
124185
if (isAxiosError(e)) {
125-
const error = e as AxiosError
126-
const response = error.response as AxiosResponse
127-
return { status: response.status, body: response.data }
186+
const error = e as AxiosError;
187+
const response = error.response as AxiosResponse;
188+
return { status: response.status, body: response.data };
128189
}
129-
throw e
190+
throw e;
130191
}
131192
},
132-
})
133-
}
193+
});
194+
};
134195
}
135196
```
136197

137-
:::
198+
:::

0 commit comments

Comments
 (0)