Skip to content

Commit 48b138d

Browse files
Add new TsRestHandler Nest API Proposal (#269)
Co-authored-by: Youssef Gaber <1728215+Gabrola@users.noreply.github.com>
1 parent fcf877d commit 48b138d

40 files changed

Lines changed: 3878 additions & 680 deletions

‎.changeset/five-phones-smile.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/nest': minor
4+
---
5+
6+
Add new `SingleHandler` and `MultiHandler` API to `@ts-rest/nest`
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11
import { InstallTabs } from '@site/src/components/InstallTabs';
2+
import { NestLegacyTransformer } from '@site/src/components/NestLegacyTransformer';
23

3-
# Nest
4+
# Nest Legacy
45

5-
## Installation
6+
<img
7+
src="https://media4.giphy.com/media/f9vYuluNDnDMsFd0ih/giphy.gif?cid=ecf05e47tfi6yxx0x0ql4jheamdthlioriywftb4es8aj026&ep=v1_gifs_search&rid=giphy.gif&ct=g"
8+
style={{
9+
display: 'block',
10+
margin: 'auto',
11+
width: '50%',
12+
height: '50%',
13+
marginBottom: '2rem',
14+
width: '300px',
15+
}}
16+
/>
17+
18+
As part of the [v3.26.0](https://github.com/ts-rest/ts-rest) we released the new `SingleHandler` and `MultiHandler` approaches to implementing your API contracts.
19+
20+
We don't have any immediate plans to deprecate the old approach, but we do recommend using the new approach as it is more flexible and allows for more control over your API implementation.
21+
22+
If we do make a v4 release, we will likely deprecate the old approach.
23+
24+
:::note
25+
26+
<NestLegacyTransformer />
27+
::: ## Installation
628

729
<InstallTabs packageName="@ts-rest/nest" />
830

‎apps/docs/docs/nest/nest.mdx‎

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { InstallTabs } from '@site/src/components/InstallTabs';
2+
3+
# Nest 🆕
4+
5+
:::info
6+
As part of `@ts-rest/nest` 3.26.0 we introduced the `single-handler` and `multi-handler` approach to Nest with ts-rest, for the legacy approach please see [here](/docs/nest/legacy/).
7+
8+
The old API will likely be deprecated in 4.0.0, so we recommend using the new API although there is no rush to migrate.
9+
:::
10+
11+
:::tip
12+
If you're migrating an existing ts-rest codebase use our **automatic migration tools** in the legacy docs [here](/docs/nest/legacy/).
13+
:::
14+
15+
## Installation
16+
17+
<InstallTabs packageName="@ts-rest/nest" />
18+
19+
## Approaches
20+
21+
Nest has always been a tricky framework to integrate with ts-rest, due to the way Nest handles routing and controllers. Nest normally uses large amounts of untyped decorators for each route, with ts-rest we introduce two new approaches to handle most of the heavy lifting for you.
22+
23+
### Single Handler Approach - Easy Migration and Flexibility ⚡️
24+
25+
We reccomend the single handler approach as it provides a 1-1 migration stategy for legacy controllers, you can swap one Nest route for one ts-rest route without affecting other routes in a Controller.
26+
27+
```typescript
28+
import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
29+
30+
@Controller()
31+
export class MyController {
32+
constructor(private readonly service: Service) {}
33+
34+
@TsRestHandler(c.getPost)
35+
async getPost() {
36+
return tsRestHandler(c.getPost, async ({ params }) => {
37+
const post = await this.service.getPost(params.id);
38+
39+
if (!post) {
40+
return { status: 404, body: null };
41+
}
42+
43+
return { status: 200, body: post };
44+
});
45+
}
46+
47+
@TsRestHandler(c.getPosts)
48+
async getPosts() {
49+
return tsRestHandler(c.getPosts, async () => {
50+
const posts = await this.service.getPosts();
51+
52+
return { status: 200, body: posts };
53+
});
54+
}
55+
}
56+
```
57+
58+
:::note
59+
Just like in regular Nest endpoints the names of the methods do not matter, you can name them whatever you like.
60+
:::
61+
62+
You may be wondering why do we return `tsRestHandler` from this method, all this function does is return the second argument. The _only_ reason this exists is to provide you a fully typed `implementation` object which matches 1-1 with the way the `@ts-rest/next`, `@ts-rest/express` and `@ts-rest/fastify` packages work.
63+
64+
#### Benefits
65+
66+
- 1-1 migration strategy
67+
- Able to not have a one to one relationship between your contract and controllers
68+
- You are able to implement one ts-rest contract across multiple controllers based on your domain.
69+
70+
### Multi Handler Approach - Ultimate Type Safety 🛡️
71+
72+
The multi handler approach is fantastic for those of you who tend towards a more functional approach to your code, or if you want to ensure that your controller methods are **always** returning the correct response types.
73+
74+
```typescript
75+
import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
76+
77+
@Controller()
78+
export class MyController {
79+
constructor(private readonly service: Service) {}
80+
81+
@TsRestHandler(c)
82+
async handler() {
83+
return tsRestHandler(c, {
84+
getPost: async ({ params }) => {
85+
const post = await this.service.getPost(params.id);
86+
87+
if (!post) {
88+
return { status: 404, body: null };
89+
}
90+
91+
return { status: 200, body: post };
92+
},
93+
getPosts: async () => {
94+
const posts = await this.service.getPosts();
95+
96+
return { status: 200, body: posts };
97+
},
98+
});
99+
}
100+
}
101+
```
102+
103+
The key difference here is you pass the entire contract (or a subset of a contract by defining a new contract or spreading multiple contracts) to the `@TsRestHandler` decorator and the `tsRestHandler` function.
104+
105+
#### Benefits
106+
107+
- Less room for error, you are forced to implement all routes in a contract
108+
- Less boilerplate
109+
- Easier to move code between `@ts-rest/next`, `@ts-rest/express` and `@ts-rest/fastify` servers
110+
111+
## Using Nest Decorators
112+
113+
Despite using `@TsRestHandler` rather than `@Get` or `@Post` (etc.) you can still use all of the existing Nest decorators on your controller methods.
114+
115+
```typescript
116+
import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
117+
118+
@Controller()
119+
export class MyController {
120+
constructor(private readonly service: Service) {}
121+
122+
@TsRestHandler(c.test)
123+
async myMethod(@Req() req: Request) {
124+
return tsRestHandler(c.test, async ({ params }) => {
125+
// ...
126+
127+
console.log(req.headers);
128+
// ^ You can still use Nest decorators
129+
});
130+
}
131+
}
132+
```
133+
134+
In this example above we're using the `@Req` decorator to get access to the `Request` object, this is a great example of how you can use Nest decorators to get access to the underlying request/response objects.
135+
136+
This isn't limited to Param decorators, you can use any Nest decorator on your controllers as you would normally.
137+
138+
## Throwing Type-Safe Errors 🍀
139+
140+
Generally speaking you should try to return all errors as a response, so that you get the benefits of `ts-rest` typing in the contract (to let your consumers know what errors to expect).
141+
142+
However, sometimes it's much cleaner and simpler to throw an error rather than passing a response back to your handler - the problem is that you normally **loose all type safety** when you do this.
143+
144+
With `TsRestException` you can throw a Nest exception and it will be handled by the `tsRestHandler` function and returned as a fully typed response.
145+
146+
```typescript
147+
throw new TsRestException(contract.test, {
148+
status: 400,
149+
body: { code: 'UserAlreadySignedUp', message: 'User has already signed up' },
150+
});
151+
```
152+
153+
This example above will provide you autocomplete for the given `status` within the `contract.test` responses contract.
154+
155+
:::caution
156+
157+
The only risk of doing it this way is that you throw the wrong exception for a given route, which can happen if you throw from a piece of code used by multiple routes. If you want to be 100% sure you are throwing the correct exception you should use the `tsRestHandler` function to return the response.
158+
159+
This can be somewhat mitigated if you share the error code schema between multiple routes, so that you can't throw the wrong error code.
160+
161+
:::
162+
163+
## Configuration
164+
165+
To configure certain ts-rest options you can use the `@TsRest` decorator on either your Controller or use the existing `@Api` decorator.
166+
167+
### JSON Query Parameters
168+
169+
To handle JSON query parameters, pass the `jsonQuery` option to the `@TsRest` decorator on your entire controller.
170+
171+
```typescript
172+
import { TsRest } from '@ts-rest/nest';
173+
174+
@Controller()
175+
@TsRest({ jsonQuery: true })
176+
export class MyController {}
177+
```
178+
179+
You can also use the method decorator to override or configure options for a specific route.
180+
181+
```typescript
182+
import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
183+
184+
@Controller()
185+
export class MyController {
186+
constructor(private readonly service: Service) {}
187+
188+
@TsRestHandler(c.getPost, { jsonQuery: true })
189+
async getPost() {
190+
return tsRestHandler(c.getPost, async () => {
191+
// ...
192+
});
193+
}
194+
}
195+
```
196+
197+
### Response Validation
198+
199+
You can enable response validation by passing the `validateResponses` option to the `@TsRest()` decorator.
200+
This will enable response parsing and validation for a returned status code, if there is a corresponding response Zod schema defined in the contract.
201+
This is useful for ensuring absolute safety that your controller is returning the correct response types as well as stripping any extra properties.
202+
203+
```typescript
204+
import { TsRest } from '@ts-rest/nest';
205+
206+
@Controller()
207+
@TsRest({ validateResponses: true })
208+
export class MyController {}
209+
```
210+
211+
You can also use the method decorator to override or configure options for a specific route.
212+
213+
```typescript
214+
import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
215+
216+
@Controller()
217+
export class MyController {
218+
constructor(private readonly service: Service) {}
219+
220+
@TsRestHandler(c.getPost, { validateResponses: true })
221+
async getPost() {
222+
return tsRestHandler(c.getPost, async () => {
223+
// ...
224+
});
225+
}
226+
}
227+
```
228+
229+
If validation fails a `ResponseValidationError` will be thrown causing a 500 response to be returned.
230+
You can catch this error and handle it as you wish by using a [NestJS exception filter](https://docs.nestjs.com/exception-filters).
231+
232+
## Gotchas
233+
234+
:::caution
235+
236+
Currently any existing Nest global prefix, versioning, or controller prefixes will be ignored, please see https://github.com/ts-rest/ts-rest/issues/70 for more details.
237+
238+
If this feature is highly requested, we can investigate a solution.
239+
240+
We have added the ability to [prefix paths](/docs/core/#path-prefix), allowing more flexibility in defining your API endpoints. This can be used as a workaround for this functionality.
241+
:::

‎apps/docs/docusaurus.config.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const lightCodeTheme = require('prism-react-renderer/themes/github');
2-
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
2+
const darkCodeTheme = require('prism-react-renderer/themes/vsDark');
33

44
const vercelAttribution = `
55
<div style="padding-top: 25px;">

‎apps/docs/project.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@
1313
"serve": {
1414
"executor": "@nx-plus/docusaurus:dev-server",
1515
"options": {
16-
"port": 3000
16+
"port": 3000,
17+
"host": "::"
18+
}
19+
},
20+
"test": {
21+
"executor": "@nx/vite:test",
22+
"outputs": ["coverage/apps/docs"],
23+
"options": {
24+
"passWithNoTests": true,
25+
"reportsDirectory": "../../coverage/apps/docs"
1726
}
1827
}
1928
},

‎apps/docs/sidebars.js‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,13 @@ const sidebars = {
7777
id: 'react-query',
7878
},
7979
{
80-
type: 'doc',
80+
type: 'category',
8181
label: '@ts-rest/nest',
82-
id: 'nest',
82+
collapsed: false,
83+
items: [
84+
{ type: 'doc', id: 'nest/nest' },
85+
{ type: 'doc', id: 'nest/legacy' },
86+
],
8387
},
8488
{
8589
type: 'doc',

‎apps/docs/src/components/InstallTabs.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import React from 'react';
66
export const InstallTabs = ({ packageName }: { packageName: string }) => {
77
return (
88
<Tabs groupId="packageManager">
9+
<TabItem value="pnpm" label="pnpm">
10+
<CodeBlock language="bash">pnpm add {packageName}</CodeBlock>
11+
</TabItem>
912
<TabItem value="npm" label="npm">
1013
<CodeBlock language="bash">npm install {packageName}</CodeBlock>
1114
</TabItem>
1215
<TabItem value="yarn" label="yarn">
1316
<CodeBlock language="bash">yarn add {packageName}</CodeBlock>
1417
</TabItem>
15-
<TabItem value="pnpm" label="pnpm">
16-
<CodeBlock language="bash">pnpm add {packageName}</CodeBlock>
17-
</TabItem>
1818
</Tabs>
1919
);
2020
};

0 commit comments

Comments
 (0)