# nest.js tRPC API建置 tRPC 是一個基於 TypeScript 的遠端過程呼叫(RPC)框架,簡化client端與server端之間的通訊,並提供高效率的類型安全性。它允許使用類似本地函數呼叫的方式來呼叫遠端函數,同時自動處理序列化和反序列化、錯誤處理和通訊協定等底層細節。 ## 套件安裝 ```bash! npm install nestjs-trpc zod npm install trpc-panel ``` ## 基本資訊 使用tRPC裡有3個構成要素,router、context、middleware,個核心概念有助於建立類型安全且高效的 API。 * **Router**: 用於定義和組織 tRPC 的程式(procedures),類似於傳統的控制器。透過在類別上使用 @Router() 裝飾器,並在方法上使用 @Query() 或 @Mutation() 裝飾器,可以定義查詢或變更操作。 * **Context**: 是在 tRPC 中用於共用請求範圍內的資料的物件。在 nestjs-trpc 中,可以透過實作 TRPCContext 介面來定義內容。允許在程式執行期間​​存取共享的資料或服務。 * **Middleware**:是在請求處理之前或之後執行的函數,用於添加額外的功能或修改請求和回應。在 nestjs-trpc 中,可以透過實作 TRPCMiddleware 介面來建立中間件 ## 建置context context為定義tRPC的數據,所有傳輸都會經過context的定義再回饋於middleware跟router,首先建置context的ts檔,並且聲明需導入的傳輸項目 ==src\dogs\dogs.context.ts== ```typescript! import { Injectable } from '@nestjs/common'; import { ContextOptions, TRPCContext } from 'nestjs-trpc'; @Injectable() export class DogsContext implements TRPCContext { create(opts: ContextOptions): Record<string, unknown> { return { isContextApplied: true, req: opts.req, res: opts.res, //設定可以於middleware讀取到req跟res訊息 }; } } ``` ## 建置route route先廸立最簡單的查詢,定義findAll跟findQuery,findQuery可以帶入input參數 ==dogs.router.ts== ```typescript! import { Router, Query, Input, UseMiddlewares } from 'nestjs-trpc'; import { z } from 'zod'; @Router({ alias: 'trpcs' }) export class DogsRouter { constructor() { } @Query() async findAll() { return `findAll Hello at ${new Date().toISOString()}` } @Query({ input: z.object({ name: z.string() }) }) async findQuery(@Input('name') name: string) { console.log('name:', name) return `findQuery Hello ${name} at ${new Date().toISOString()}` } } ``` ## module設定 module檔案裡需要imports套件TRPCModule才能產生運作tRPC的server.ts ==dogs.module.ts== ```typescript! import { Module } from '@nestjs/common'; import { DogsRouter } from './dogs.router'; import { TRPCModule } from 'nestjs-trpc'; import { DogsController } from './dogs.controller'; import { DogsContext } from './dogs.context'; @Module({ imports: [ TRPCModule.forRoot({ autoSchemaFile: 'production', //這裡於build時會產生production目錄及server.ts檔 context: DogsContext //這裡要定義運作的Context }), ], controllers: [DogsController], providers: [DogsRouter, DogsContext] }) export class DogsModule { } ``` ## panel使用進行測試 panel為server端產生tRPC測試的頁面 ```typescript! import { Controller, OnModuleInit, Inject, All } from '@nestjs/common'; import { AppRouterHost } from 'nestjs-trpc'; import { renderTrpcPanel } from 'trpc-panel'; import { AnyRouter } from '@trpc/server'; @Controller('dogs') export class DogsController implements OnModuleInit { private appRouter: AnyRouter; constructor( @Inject(AppRouterHost) private readonly appRouterHost: AppRouterHost, ) { } onModuleInit() { this.appRouter = this.appRouterHost.appRouter; } @All('panel') panel(): string { return renderTrpcPanel(this.appRouter, { url: 'http://localhost:3000/trpc', //設定自身的連結,trpc為固定router }); } } ``` 運作後可以直接於瀏覽器http://localhost:3000/dogs/panel查看所產生的介面 ![image](https://hackmd.io/_uploads/rJcrU30skg.png) ## middleware middleware為濾tRPC傳輸的驗證,通常為像auth的登入驗證 ==src\dogs\dogs.middleware.ts== ```typescript! import { MiddlewareOptions, MiddlewareResponse, TRPCMiddleware, } from 'nestjs-trpc'; import { Injectable } from '@nestjs/common'; import { TRPCError } from '@trpc/server'; @Injectable() export class DogsMiddleware implements TRPCMiddleware { constructor() { } async use(opts: MiddlewareOptions<{ req: Request }>): Promise<MiddlewareResponse> { const start = Date.now(); const result = await opts.next(); //測試驗證如果有Error字串會回傳UNAUTHORIZED if (opts.rawInput?.['name'] === 'Error') { throw new TRPCError({ message: "No rawInput found.", code: "UNAUTHORIZED" }); } const durationMs = Date.now() - start; const meta = { path: opts.path, type: opts.type, durationMs }; result.ok ? console.log('OK request timing:', meta) : console.error('Non-OK request timing', meta); return result; } } ``` 於router裡加入UseMiddlewares驗證 ==src\dogs\dogs.router.ts== ```typescript! ...以上省略 @UseMiddlewares(DogsMiddleware) @Query({ input: z.object({ name: z.string() }) }) async findUser(@Input('name') name: string) { console.log('name:', name) return `findQuery Hello ${name} at ${new Date().toISOString()}` } ``` 別忘了module裡需要providers導入Middleware ==src\dogs\dogs.module.ts== ```typescript! providers: [DogsService, DogsRouter, DogsContext, DogsMiddleware] }) ``` 進行測試時帶入字串Error則會res出401錯誤 ![image](https://hackmd.io/_uploads/BJvxt3Rike.png) 最後,要將build的"production\server.ts"檔案丟給client端使用,client才能夠讀取server.ts的設定進行傳輸。 ## 結尾廢言 tRPC如果用於傳輸協定,其效率會比使用GraphQL來得好,多了安全性及型別驗證功能,就可以避免許多不必要的問題。但如果server跟cleint為分離開發,都要用typescript才能運作,如果是用其他語法會需要更多轉換的方式,代價並不少。 不管是tRPC或GraphQL,最終都是以RESTFUL API來進行傳輸,並不像gRpc用Protocol Buffers傳輸,所以對client端而言,即然都是用REST,為何還要繞一圈來使用,增加彼此的學習成本。 所以,要怎麼說服client端使用,應該會比提供技術的運用,來的更麻煩。 參考: > https://angularexperts.io/blog/angular-trpc?utm_source=chatgpt.com > https://www.nestjs-trpc.io/