Skip to content

Commit c4fb3f6

Browse files
feat(core): Support application/x-www-form-urlencoded content type (#461)
Co-authored-by: Michael Angelo Rivera <55844504+michaelangeloio@users.noreply.github.com>
1 parent de90c1a commit c4fb3f6

4 files changed

Lines changed: 63 additions & 2 deletions

File tree

.changeset/thin-numbers-taste.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+
feat: `@ts-rest/core`: Add support for `x-www-form-urlencoded` content-type to core client

libs/ts-rest/core/src/lib/client.spec.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ const postsRouter = c.router({
6161
authorId: z.string(),
6262
}),
6363
},
64+
createPostXForm: {
65+
method: 'POST',
66+
path: '/posts',
67+
responses: {
68+
200: c.response<Post>(),
69+
},
70+
body: z.string(),
71+
contentType: 'application/x-www-form-urlencoded'
72+
},
6473
mutationWithQuery: {
6574
method: 'POST',
6675
path: '/posts',
@@ -325,7 +334,7 @@ describe('client', () => {
325334
published: true,
326335
filter: { title: 'test' },
327336
},
328-
337+
329338
});
330339

331340
expect(result.body).toStrictEqual(value);
@@ -436,6 +445,32 @@ describe('client', () => {
436445
expect(result.headers.get('Content-Type')).toBe('application/json');
437446
});
438447

448+
it('w/ body and content-type header', async () => {
449+
const value = "key=value";
450+
fetchMock.postOnce(
451+
{
452+
url: 'https://api.com/posts',
453+
headers: {
454+
"content-type": "application/x-www-form-urlencoded"
455+
}
456+
},
457+
{
458+
body: value,
459+
status: 200,
460+
headers: {
461+
"content-type": "application/x-www-form-urlencoded"
462+
}
463+
}
464+
);
465+
466+
const result = await client.posts.createPostXForm({
467+
body: "key=value",
468+
});
469+
470+
expect(result.status).toBe(200);
471+
expect(result.headers.get('Content-Type')).toBe('application/x-www-form-urlencoded');
472+
});
473+
439474
it('w/ query params', async () => {
440475
fetchMock.postOnce(
441476
{

libs/ts-rest/core/src/lib/client.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,27 @@ export const fetchApi = ({
221221
});
222222
}
223223

224+
if (route.method !== 'GET' && route.contentType === 'application/x-www-form-urlencoded') {
225+
const headers = {
226+
...combinedHeaders,
227+
'content-type': 'application/x-www-form-urlencoded',
228+
}
229+
return apiFetcher({
230+
route,
231+
path,
232+
method: route.method,
233+
credentials: clientArgs.credentials,
234+
headers,
235+
body: body instanceof FormData ? body : createFormData(body),
236+
rawBody: body,
237+
rawQuery: query,
238+
contentType: 'application/x-www-form-urlencoded',
239+
signal,
240+
next,
241+
...extraInputArgs,
242+
});
243+
}
244+
224245
const includeContentTypeHeader =
225246
route.method !== 'GET' && body !== null && body !== undefined;
226247

libs/ts-rest/core/src/lib/dsl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export type AppRouteQuery = AppRouteCommon & {
5454
*/
5555
export type AppRouteMutation = AppRouteCommon & {
5656
method: 'POST' | 'DELETE' | 'PUT' | 'PATCH';
57-
contentType?: 'application/json' | 'multipart/form-data';
57+
contentType?: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded';
5858
body: ContractAnyType;
5959
};
6060

0 commit comments

Comments
 (0)