Skip to content

Commit 0f1edf9

Browse files
authored
refactor: revise some namings in Nest (#140)
* refactor: revise some namings in Nest * fix: JsonQuery method decorator not overriding class decorator * chore: changeset * fix: Nest deprecation warning for Zod errors * docs: update and fix badge links * docs: add link to docs in deprecation message * refactor: rename TypedRequest into TsRestRequest * chore: update documentation and release notes
1 parent 822b1a1 commit 0f1edf9

File tree

15 files changed

+322
-205
lines changed

15 files changed

+322
-205
lines changed

.changeset/dull-maps-draw.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@ts-rest/nest': minor
3+
'@ts-rest/core': patch
4+
---
5+
6+
Rename some Nest functions and types, and deprecate old names
7+
8+
Fix Nest deprecation warning when passing Zod error to HttpException (#122)
9+
10+
Some internal helper types (`NestControllerShapeFromAppRouter` and `NestAppRouteShape`) that were previously exported are now kept internal.
11+
You can use `NestControllerInterface` and `NestRequestShapes` instead.

README.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,24 @@
44
<img src="https://avatars.githubusercontent.com/u/109956939?s=400&u=8bf67b1281da46d64eab85f48255cd1892bf0885&v=4" height="150"></img>
55
</p>
66

7-
<p align="center">RPC-like client and server helpers for a magical end to end typed experience</p>
8-
<p align="center">
9-
<a href="https://www.npmjs.com/package/@ts-rest/core">
10-
<img src="https://img.shields.io/npm/v/@ts-rest/core.svg" alt="langue typescript"/>
11-
</a>
12-
<a href="https://www.npmjs.com/package/@ts-rest/core"/>
13-
<img alt="npm" src="https://img.shields.io/npm/dw/@ts-rest/core"/>
14-
<a href="https://github.com/ts-rest/ts-rest/blob/main/LICENSE"></a>
15-
<img alt="GitHub" src="https://img.shields.io/github/license/ts-rest/ts-rest"/>
16-
<img alt="GitHub Workflow Status" src="https://img.shields.io/bundlephobia/minzip/@ts-rest/core?label=%40ts-rest%2Fcore"/>
17-
<img alt="GitHub Workflow Status" src="https://img.shields.io/discord/1055855205960392724"/>
18-
19-
</p>
7+
<p align="center">RPC-like client and server helpers for a magical end to end typed experience</p>
8+
9+
<p align="center">
10+
<a href="https://www.npmjs.com/package/@ts-rest/core">
11+
<img src="https://img.shields.io/npm/v/@ts-rest/core.svg" alt="langue typescript"/>
12+
</a>
13+
<img alt="Github Workflow Status" src="https://img.shields.io/github/actions/workflow/status/ts-rest/ts-rest/release.yml?branch=main"/>
14+
<a href="https://www.npmjs.com/package/@ts-rest/core">
15+
<img alt="npm" src="https://img.shields.io/npm/dw/@ts-rest/core"/>
16+
</a>
17+
<a href="https://github.com/ts-rest/ts-rest/blob/main/LICENSE">
18+
<img alt="License" src="https://img.shields.io/github/license/ts-rest/ts-rest"/>
19+
</a>
20+
<img alt="Bundle Size" src="https://img.shields.io/bundlephobia/minzip/@ts-rest/core?label=%40ts-rest%2Fcore"/>
21+
<a href="https://discord.com/invite/2Megk85k5a">
22+
<img alt="Discord" src="https://img.shields.io/discord/1055855205960392724"/>
23+
</a>
24+
</p>
2025

2126
# Introduction
2227

apps/docs/docs/core/form-data.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,11 @@ With Nest this is a pretty simple implementation, due to the extensible Decorato
112112
```ts
113113
// nest
114114
@Controller()
115-
export class AppController implements ControllerShape {
115+
export class AppController implements NestControllerInterface<typeof c> {
116116
@Api(s.route.updateUserAvatar)
117117
@UseInterceptors(FileInterceptor('avatar'))
118118
async updateUserAvatar(
119-
@ApiDecorator() { params: { id } }: RouteShape['updateUserAvatar'],
119+
@TsRestRequest() { params: { id } }: RequestShapes['updateUserAvatar'],
120120
@UploadedFile() avatar: Express.Multer.File
121121
) {
122122
return {

apps/docs/docs/nest.md

Lines changed: 55 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
# Nest
22

3-
By default Nest doesn't offer a nice way to ensure type safe controllers, primarily because it's decorator driven rather than functional, like Express.
3+
As opposed to the Express implementation, where we can infer types from a function signature, we need to explicitly define our types since Nest controllers are implemented as classes.
44

55
```typescript
6-
const s = initNestServer(apiBlog);
7-
type ControllerShape = typeof s.controllerShape;
8-
type RouteShape = typeof s.routeShapes;
9-
type ResponseShapes = typeof s.responseShapes;
6+
import {
7+
Api,
8+
nestControllerContract,
9+
NestControllerInterface,
10+
NestRequestShapes,
11+
TsRestRequest,
12+
} from '@ts-rest/nest';
13+
14+
const c = nestControllerContract(apiBlog);
15+
type RequestShapes = NestRequestShapes<typeof c>;
1016

1117
@Controller()
12-
export class PostController implements ControllerShape {
18+
export class PostController implements NestControllerInterface<typeof c> {
1319
constructor(private readonly postService: PostService) {}
1420

15-
@Api(s.route.getPost)
16-
async getPost(@ApiDecorator() { params: { id } }: RouteShape['getPost']) {
21+
@Api(c.getPost)
22+
async getPost(@TsRestRequest() { params: { id } }: RequestShapes['getPost']) {
1723
const post = await this.postService.getPost(id);
1824

1925
if (!post) {
@@ -25,9 +31,17 @@ export class PostController implements ControllerShape {
2531
}
2632
```
2733

34+
The `nestControllerContract` filters your contract to only include immediate routes and no nested routes. Otherwise, you'd have to implement the entire contract in a single controller. As a result, it's good practice to design your contract into multiple nested contracts, one for each controller.
35+
36+
To implement a nested contract, you can simply call `nestControllerContract(contract.nestedContract)`
37+
38+
Having the controller class implement `NestControllerInterface` ensures that your controller implements all the routes defined in the contract. In addition, it ensures the type safety of the responses returned.
39+
2840
The `@Api` decorator takes the route, defines the path and method for the controller route.
2941

30-
It also injects "appRoute" into the req object, allowing the `@ApiDecorator` decorator automatically parse and check the query and body parameters.
42+
The `@TsRestRequest` decorator takes the contract route defined in the `@Api` decorator, and returns the parsed and validated (if using Zod) request params, query and body.
43+
44+
As Typescript cannot infer class method parameter types from an implemented interface, we need to explicitly define the type for the request parameter using the `NestRequestShapes` type.
3145

3246
## JSON Query Parameters
3347

@@ -36,63 +50,61 @@ To handle JSON query parameters, you can use the `@JsonQuery()` decorator on eit
3650
```typescript
3751
@Controller()
3852
@JsonQuery()
39-
export class PostController implements ControllerShape {}
53+
export class PostController implements NestControllerInterface<typeof c> {}
4054
```
4155

4256
The method decorator can be useful to override the controller's behaviour on a per-endpoint basis.
4357

4458
```typescript
4559
@Controller()
4660
@JsonQuery()
47-
export class PostController implements ControllerShape {
61+
export class PostController implements NestControllerInterface<typeof c> {
4862
constructor(private readonly postService: PostService) {}
4963

5064
@Api(s.route.getPost)
5165
@JsonQuery(false)
52-
async getPost(@ApiDecorator() { params: { id } }: RouteShape['getPost']) {
66+
async getPost(@TsRestRequest() { params: { id } }: RequestShapes['getPost']) {
5367
// ...
5468
}
5569
}
5670
```
5771

58-
## Response Return Type Safety
59-
60-
You have two options to ensure HTTP type safety on your Nest Controllers:
61-
62-
- `ControllerShape` as shown above:
63-
- Your controller can implement the `ControllerShape` derived from `typeof s.controllerShape`
64-
- This ensures your controller methods also align with the base interface shapes
65-
- `ResponseShapes`:
72+
## Explicit Response Type Safety
6673

67-
- Your controller can utilize `typeof s.responseShapes` in the return type. For example:
74+
In cases where you do not want to implement `NestControllerInterface`.
75+
Say, if you were to implement a contract's non-nested routes across multiple controllers, or use different class method names than the ones defined in your contract, you can still ensure type safety of the responses by using the `NestResponseShapes` type.
6876

69-
```typescript
70-
const s = initNestServer(apiBlog);
71-
type ControllerShape = typeof s.controllerShape;
72-
type RouteShape = typeof s.routeShapes;
73-
type ResponseShapes = typeof s.responseShapes; // <- http Responses defined in contract
74-
75-
@Controller()
76-
export class PostController {
77-
constructor(private readonly postService: PostService) {}
77+
```typescript
78+
import {
79+
Api,
80+
nestControllerContract,
81+
NestRequestShapes,
82+
NestResponseShapes,
83+
TsRestRequest,
84+
} from '@ts-rest/nest';
85+
86+
const c = nestControllerContract(apiBlog);
87+
type RequestShapes = NestRequestShapes<typeof c>;
88+
type ResponseShapes = NestResponseShapes<typeof c>;
7889

79-
@Api(s.route.getPost)
80-
async getPost(
81-
@ApiDecorator() { params: { id } }: RouteShape['getPost']
82-
): Promise<ResponseShapes['getPost']> {
83-
// <- return type defined here
84-
const post = await this.postService.getPost(id);
90+
@Controller()
91+
export class PostController {
92+
constructor(private readonly postService: PostService) {}
8593

86-
if (!post) {
87-
return { status: 404, body: null };
88-
}
94+
@Api(c.getPost)
95+
async getPost(
96+
@TsRestRequest() { params: { id } }: RequestShapes['getPost']
97+
): Promise<ResponseShapes['getPost']> {
98+
const post = await this.postService.getPost(id);
8999

90-
return { status: 200, body: post };
91-
}
100+
if (!post) {
101+
return { status: 404 as const, body: null };
92102
}
93-
```
94103

95-
- If your controller needs to implement a difference class, or needs extra methods defined outside of a contract, this is option gives that flexibility without having to worry about maintaining class extensions.
104+
return { status: 200 as const, body: post };
105+
}
106+
}
107+
```
96108

97109
:::caution
98110

apps/docs/docs/quickstart.mdx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,23 +104,22 @@ normally Nest APIs are extremely powerful, but hard to make type safe.</p>
104104
```typescript
105105
// post.controller.ts
106106

107-
const s = initNestServer(contract);
108-
type ControllerShape = typeof s.controllerShape;
109-
type RouteShape = typeof s.routeShapes;
107+
const c = nestControllerContract(apiBlog);
108+
type RequestShapes = NestRequestShapes<typeof c>;
110109

111110
@Controller()
112-
export class PostController implements ControllerShape {
111+
export class PostController implements NestControllerInterface<typeof c> {
113112
constructor(private readonly postService: PostService) {}
114113

115-
@Api(s.route.getPost)
116-
async getPost(@ApiDecorator() { params: { id } }: RouteShape['getPost']) {
114+
@Api(c.getPost)
115+
async getPost(@TsRestRequest() { params: { id } }: RequestShapes['getPost']) {
117116
const post = await this.postService.getPost(id);
118117

119118
return { status: 200 as const, body: post };
120119
}
121120

122-
@Api(s.route.createPost)
123-
async createPost(@ApiDecorator() { body }: RouteShape['createPost']) {
121+
@Api(c.createPost)
122+
async createPost(@TsRestRequest() { body }: RequestShapes['createPost']) {
124123
const post = await this.postService.createPost({
125124
title: body.title,
126125
body: body.body,

apps/example-microservice/users-service/src/app/app.controller.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import { Controller, UploadedFile, UseInterceptors } from '@nestjs/common';
22
import { usersApi } from '@ts-rest/example-microservice/util-users-api';
3-
import { Api, ApiDecorator, initNestServer } from '@ts-rest/nest';
3+
import {
4+
Api,
5+
TsRestRequest,
6+
nestControllerContract,
7+
NestControllerInterface,
8+
NestRequestShapes,
9+
} from '@ts-rest/nest';
410
import { AppService } from './app.service';
511
import { FileInterceptor } from '@nestjs/platform-express';
612
import 'multer';
713

8-
const s = initNestServer(usersApi);
9-
type ControllerShape = typeof s.controllerShape;
10-
type RouteShape = typeof s.routeShapes;
14+
const c = nestControllerContract(usersApi);
15+
type RequestShapes = NestRequestShapes<typeof c>;
1116

1217
@Controller()
13-
export class AppController implements ControllerShape {
18+
export class AppController implements NestControllerInterface<typeof c> {
1419
constructor(private readonly appService: AppService) {}
1520

16-
@Api(s.route.getUser)
17-
async getUser(@ApiDecorator() { params: { id } }: RouteShape['getUser']) {
21+
@Api(c.getUser)
22+
async getUser(@TsRestRequest() { params: { id } }: RequestShapes['getUser']) {
1823
return {
1924
status: 200 as const,
2025
body: {
@@ -25,10 +30,10 @@ export class AppController implements ControllerShape {
2530
};
2631
}
2732

28-
@Api(s.route.updateUserAvatar)
33+
@Api(c.updateUserAvatar)
2934
@UseInterceptors(FileInterceptor('avatar'))
3035
async updateUserAvatar(
31-
@ApiDecorator() { params: { id } }: RouteShape['updateUserAvatar'],
36+
@TsRestRequest() { params: { id } }: RequestShapes['updateUserAvatar'],
3237
@UploadedFile() avatar: Express.Multer.File
3338
) {
3439
return {

apps/example-nest/src/app/post-json-query.controller.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { Controller } from '@nestjs/common';
22
import { apiBlog } from '@ts-rest/example-contracts';
3-
import { Api, ApiDecorator, initNestServer, JsonQuery } from '@ts-rest/nest';
3+
import {
4+
Api,
5+
TsRestRequest,
6+
JsonQuery,
7+
nestControllerContract,
8+
NestControllerInterface,
9+
NestRequestShapes,
10+
} from '@ts-rest/nest';
411
import { z } from 'zod';
512
import { PostService } from './post.service';
613

7-
const s = initNestServer({
14+
const c = nestControllerContract({
815
getPosts: {
916
...apiBlog.getPosts,
1017
path: '/posts-json-query',
@@ -15,17 +22,19 @@ const s = initNestServer({
1522
}),
1623
},
1724
});
18-
type ControllerShape = typeof s.controllerShape;
19-
type RouteShape = typeof s.routeShapes;
25+
type RequestShapes = NestRequestShapes<typeof c>;
2026

2127
@JsonQuery()
2228
@Controller()
23-
export class PostJsonQueryController implements ControllerShape {
29+
export class PostJsonQueryController
30+
implements NestControllerInterface<typeof c>
31+
{
2432
constructor(private readonly postService: PostService) {}
2533

26-
@Api(s.route.getPosts)
34+
@Api(c.getPosts)
2735
async getPosts(
28-
@ApiDecorator() { query: { take, skip, search } }: RouteShape['getPosts']
36+
@TsRestRequest()
37+
{ query: { take, skip, search } }: RequestShapes['getPosts']
2938
) {
3039
const { posts, totalPosts } = await this.postService.getPosts({
3140
take,

0 commit comments

Comments
 (0)