입력 및 출력 검증기
tRPC 프로시저는 입력 및/또는 출력을 위한 검증 로직을 정의할 수 있으며, 검증기는 입력과 출력의 타입을 추론하는 데에도 사용됩니다(사용 가능한 경우 Standard Schema 인터페이스를, 그렇지 않은 경우 지원되는 검증기의 커스텀 인터페이스를 사용). 우리는 많은 인기 있는 검증기에 대해 1급 지원을 제공하며, 직접 지원하지 않는 검증기도 통합할 수 있습니다.
입력 검증기
입력 검증기를 정의하면 tRPC는 프로시저 호출이 올바른지 확인할 수 있으며, 올바르지 않은 경우 검증 오류를 반환합니다.
입력 검증기를 설정하려면 procedure.input() 메서드를 사용합니다:
tsexport constappRouter =t .router ({hello :publicProcedure .input (z .object ({name :z .string (),}),).query ((opts ) => {constname =opts .input .name ;return {greeting : `Hello ${opts .input .name }`,};}),});
tsexport constappRouter =t .router ({hello :publicProcedure .input (z .object ({name :z .string (),}),).query ((opts ) => {constname =opts .input .name ;return {greeting : `Hello ${opts .input .name }`,};}),});
입력 병합
.input()을 연이어 사용해 더 복잡한 타입을 구성할 수 있습니다. 미들웨어에서 여러 프로시저에 공통 입력을 적용할 때 특히 유용합니다.
입력 병합은 객체 프로퍼티를 함께 전개하여 작동합니다. 이는 객체 타입만 체이닝할 수 있음을 의미하며, 비객체 타입(예: z.string())은 병합할 수 없습니다. 체이닝된 두 .input() 호출이 동일한 프로퍼티를 정의하는 경우, 후자가 우선합니다.
tsconstbaseProcedure =t .procedure .input (z .object ({townName :z .string () })).use ((opts ) => {constinput =opts .input ;console .log (`Handling request with user from: ${input .townName }`);returnopts .next ();});export constappRouter =t .router ({hello :baseProcedure .input (z .object ({name :z .string (),}),).query ((opts ) => {constinput =opts .input ;return {greeting : `Hello ${input .name }, my friend from ${input .townName }`,};}),});
tsconstbaseProcedure =t .procedure .input (z .object ({townName :z .string () })).use ((opts ) => {constinput =opts .input ;console .log (`Handling request with user from: ${input .townName }`);returnopts .next ();});export constappRouter =t .router ({hello :baseProcedure .input (z .object ({name :z .string (),}),).query ((opts ) => {constinput =opts .input ;return {greeting : `Hello ${input .name }, my friend from ${input .townName }`,};}),});
출력 검증기
tRPC는 프로시저의 반환 타입을 추론하여 자동 타입 안전성을 제공하므로, 출력을 검증하는 것은 입력을 정의하는 것만큼 항상 중요하지는 않습니다. 출력 검증기를 정의해야 하는 몇 가지 이유는 다음과 같습니다:
- 신뢰할 수 없는 소스에서 반환된 데이터가 올바른지 확인
- 클라이언트에게 필요한 것보다 더 많은 데이터를 반환하지 않도록 보장
출력 검증이 실패하면 서버는 INTERNAL_SERVER_ERROR로 응답합니다.
tsimport {z } from 'zod';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .output (z .object ({greeting :z .string (),}),).query ((opts ) => {return {greeting : 'hello world',};}),});
tsimport {z } from 'zod';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .output (z .object ({greeting :z .string (),}),).query ((opts ) => {return {greeting : 'hello world',};}),});
구독의 출력 검증
구독은 비동기 반복자이므로, 위의 동일한 검증 기법을 사용할 수 있습니다.
자세한 정보는 구독 가이드를 참고하세요.
가장 기본적인 검증기: 함수
3rd party 의존성 없이 함수로 검증기를 정의할 수 있습니다.
특정한 필요가 없는 한 커스텀 검증기를 만드는 것을 권장하지 않지만, 여기에는 마법이 없다는 점을 이해하는 것이 중요합니다 - 이는 _단순히 typescript_일 뿐입니다!
대부분의 경우 검증 라이브러리 사용을 권장합니다.
tsimport {initTRPC } from '@trpc/server';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input ((value ): string => {if (typeofvalue === 'string') {returnvalue ;}throw newError ('Input is not a string');}).output ((value ): string => {if (typeofvalue === 'string') {returnvalue ;}throw newError ('Output is not a string');}).query ((opts ) => {const {input } =opts ;return `hello ${input }`;}),});export typeAppRouter = typeofappRouter ;
tsimport {initTRPC } from '@trpc/server';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input ((value ): string => {if (typeofvalue === 'string') {returnvalue ;}throw newError ('Input is not a string');}).output ((value ): string => {if (typeofvalue === 'string') {returnvalue ;}throw newError ('Output is not a string');}).query ((opts ) => {const {input } =opts ;return `hello ${input }`;}),});export typeAppRouter = typeofappRouter ;
라이브러리 통합
tRPC는 Standard Schema를 따르는 라이브러리를 비롯해 널리 쓰이는 여러 검증 및 파싱 라이브러리를 별도 설정 없이 지원합니다. 아래에서는 tRPC가 공식 지원하는 검증기의 사용 예시를 살펴봅니다.
Zod 사용
Zod는 tRPC가 기본으로 권장하는 라이브러리입니다. 생태계가 탄탄해 코드베이스의 여러 영역에서 사용하기 좋습니다. 특별히 선호하는 검증기가 없고 향후 요구 사항을 제한하지 않는 강력한 라이브러리를 원한다면 Zod가 좋은 선택입니다.
tsimport {initTRPC } from '@trpc/server';import {z } from 'zod';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (z .object ({name :z .string (),}),).output (z .object ({greeting :z .string (),}),).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
tsimport {initTRPC } from '@trpc/server';import {z } from 'zod';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (z .object ({name :z .string (),}),).output (z .object ({greeting :z .string (),}),).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
Yup 사용
tsimport {initTRPC } from '@trpc/server';import * asyup from 'yup';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (yup .object ({name :yup .string ().required (),}),).output (yup .object ({greeting :yup .string ().required (),}),).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
tsimport {initTRPC } from '@trpc/server';import * asyup from 'yup';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (yup .object ({name :yup .string ().required (),}),).output (yup .object ({greeting :yup .string ().required (),}),).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
Superstruct 사용
tsimport {initTRPC } from '@trpc/server';import {object ,string } from 'superstruct';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (object ({name :string () })).output (object ({greeting :string () })).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
tsimport {initTRPC } from '@trpc/server';import {object ,string } from 'superstruct';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (object ({name :string () })).output (object ({greeting :string () })).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
scale-ts 사용
tsimport {initTRPC } from '@trpc/server';import * as$ from 'scale-codec';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input ($ .object ($ .field ('name',$ .str ))).output ($ .object ($ .field ('greeting',$ .str ))).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
tsimport {initTRPC } from '@trpc/server';import * as$ from 'scale-codec';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input ($ .object ($ .field ('name',$ .str ))).output ($ .object ($ .field ('greeting',$ .str ))).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
Typia 사용
tsimport { initTRPC } from '@trpc/server';import typia from 'typia';import { v4 } from 'uuid';import { IBbsArticle } from '../structures/IBbsArticle';const t = initTRPC.create();const publicProcedure = t.procedure;export const appRouter = t.router({store: publicProcedure.input(typia.createAssert<IBbsArticle.IStore>()).output(typia.createAssert<IBbsArticle>()).query(({ input }) => {return {id: v4(),writer: input.writer,title: input.title,body: input.body,created_at: new Date().toString(),};}),});export type AppRouter = typeof appRouter;
tsimport { initTRPC } from '@trpc/server';import typia from 'typia';import { v4 } from 'uuid';import { IBbsArticle } from '../structures/IBbsArticle';const t = initTRPC.create();const publicProcedure = t.procedure;export const appRouter = t.router({store: publicProcedure.input(typia.createAssert<IBbsArticle.IStore>()).output(typia.createAssert<IBbsArticle>()).query(({ input }) => {return {id: v4(),writer: input.writer,title: input.title,body: input.body,created_at: new Date().toString(),};}),});export type AppRouter = typeof appRouter;
ArkType 사용
tsimport { initTRPC } from '@trpc/server';import { type } from 'arktype';export const t = initTRPC.create();const publicProcedure = t.procedure;export const appRouter = t.router({hello: publicProcedure.input(type({ name: 'string' })).query((opts) => {return {greeting: `hello ${opts.input.name}`,};}),});export type AppRouter = typeof appRouter;
tsimport { initTRPC } from '@trpc/server';import { type } from 'arktype';export const t = initTRPC.create();const publicProcedure = t.procedure;export const appRouter = t.router({hello: publicProcedure.input(type({ name: 'string' })).query((opts) => {return {greeting: `hello ${opts.input.name}`,};}),});export type AppRouter = typeof appRouter;
effect 사용
tsimport {initTRPC } from '@trpc/server';import {Schema } from 'effect';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (Schema .standardSchemaV1 (Schema .Struct ({name :Schema .String }))).output (Schema .standardSchemaV1 (Schema .Struct ({greeting :Schema .String }))).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
tsimport {initTRPC } from '@trpc/server';import {Schema } from 'effect';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (Schema .standardSchemaV1 (Schema .Struct ({name :Schema .String }))).output (Schema .standardSchemaV1 (Schema .Struct ({greeting :Schema .String }))).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
With Valibot
tsimport {initTRPC } from '@trpc/server';import * asv from 'valibot';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (v .object ({name :v .string () })).output (v .object ({greeting :v .string () })).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
tsimport {initTRPC } from '@trpc/server';import * asv from 'valibot';export constt =initTRPC .create ();constpublicProcedure =t .procedure ;export constappRouter =t .router ({hello :publicProcedure .input (v .object ({name :v .string () })).output (v .object ({greeting :v .string () })).query (({input }) => {return {greeting : `hello ${input .name }`,};}),});export typeAppRouter = typeofappRouter ;
@robolex/sure 사용
필요에 따라 자체 Error 타입과 에러 발생 함수를 정의할 수 있습니다.
편의상 @robolex/sure 패키지는 sure/src/err.ts를 제공합니다:
ts// sure/src/err.tsexport const err = (schema: any) => (input: any) => {const [good, result] = schema(input);if (good) return result;throw result;};
ts// sure/src/err.tsexport const err = (schema: any) => (input: any) => {const [good, result] = schema(input);if (good) return result;throw result;};
tsimport { err, object, string } from '@robolex/sure';import { initTRPC } from '@trpc/server';export const t = initTRPC.create();const publicProcedure = t.procedure;export const appRouter = t.router({hello: publicProcedure.input(err(object({name: string,}),),).output(err(object({greeting: string,}),),).query(({ input }) => {// ^?return {greeting: `hello ${input.name}`,};}),});export type AppRouter = typeof appRouter;
tsimport { err, object, string } from '@robolex/sure';import { initTRPC } from '@trpc/server';export const t = initTRPC.create();const publicProcedure = t.procedure;export const appRouter = t.router({hello: publicProcedure.input(err(object({name: string,}),),).output(err(object({greeting: string,}),),).query(({ input }) => {// ^?return {greeting: `hello ${input.name}`,};}),});export type AppRouter = typeof appRouter;
TypeBox 사용
tsimport { Type } from '@sinclair/typebox';import { initTRPC } from '@trpc/server';import { wrap } from '@typeschema/typebox';export const t = initTRPC.create();const publicProcedure = t.procedure;export const appRouter = t.router({hello: publicProcedure.input(wrap(Type.Object({ name: Type.String() }))).output(wrap(Type.Object({ greeting: Type.String() }))).query(({ input }) => {return {greeting: `hello ${input.name}`,};}),});export type AppRouter = typeof appRouter;
tsimport { Type } from '@sinclair/typebox';import { initTRPC } from '@trpc/server';import { wrap } from '@typeschema/typebox';export const t = initTRPC.create();const publicProcedure = t.procedure;export const appRouter = t.router({hello: publicProcedure.input(wrap(Type.Object({ name: Type.String() }))).output(wrap(Type.Object({ greeting: Type.String() }))).query(({ input }) => {return {greeting: `hello ${input.name}`,};}),});export type AppRouter = typeof appRouter;
자체 Validator 라이브러리 기여하기
tRPC 사용이 가능한 validator 라이브러리를 개발 중이라면, 이 페이지에 다른 예시와 동등한 사용법과 문서 링크를 포함하여 PR을 열어 주십시오.
대부분의 tRPC 통합은 기존 타입 인터페이스 중 하나를 구현하는 것만으로 가능합니다. Standard Schema를 따르는 방식을 권장하지만, 필요한 경우 새로운 지원 인터페이스를 추가하는 PR도 검토할 수 있습니다. 먼저 이슈를 열어 논의해 주세요. 기존 파싱·검증 인터페이스와 함수는 소스 코드에서 확인할 수 있습니다.