如何在Remix、Zod和Prisma中设置tRPC

huangapple go评论91阅读模式
英文:

How to Setup tRPC with Remix, Zod and Prisma

问题

tRPC文档提供了逐步指南,适用于流行的SSR框架,例如NextJS,但大多数示例使用了NextJS适配器和中间件。没有关于如何在Remix中设置的文档,而Remix是另一个流行的SSR React框架。

英文:

tRPC documentation has step by step guide for popular SSR framework like NextJS, but most of the example uses NextJS adapters and middle-ware. There's no documentation for setting it up with Remix which is yet another popular SSR React framework.

答案1

得分: 1

Client Side:
这将是所有框架的通用设置,这里我使用superjson作为序列化的转换器-

// app/utils/api.ts
import {
  createTRPCProxyClient,
  httpLink,
  loggerLink,
} from '@trpc/client';
import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server';
import superjson from 'superjson';

import { type AppRouter } from '~/routes/api/root';

const getBaseUrl = () => {
  if (typeof window !== 'undefined') return ''; // 浏览器应使用相对 URL
  // 将其更改为指向您的 SSR 基础 URL
  return `http://localhost:${process.env.PORT ?? 3000}`; // 开发 SSR 应使用 localhost
};

export const client = createTRPCProxyClient<AppRouter>({
  transformer: superjson,
  links: [
    loggerLink({
      enabled: (opts) =>
        process.env.NODE_ENV === 'development' ||
        (opts.direction === 'down' && opts.result instanceof Error),
    }),
    httpLink({
      url: `${getBaseUrl()}/api/trpc`, // 我们需要设置服务器端 API 指向这里
    }),
  ],
});

export type RouterInputs = inferRouterInputs<AppRouter>;

export type RouterOutputs = inferRouterOutputs<AppRouter>;

Server Side:

  1. 让我们设置过程并初始化路由器 app/routes/api/trpc.ts,在这里我使用 Zod 拦截请求进行类型检查,您还可以在此处添加身份验证层:
import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
import { prisma } from '~/utils/prisma.server';

type CreateContextOptions = Record<string, never>;

export const createTRPCContext = ({
  req,
  resHeaders,
}: FetchCreateContextFnOptions) => {
  return {
    prisma,
  };
};

import { inferAsyncReturnType, initTRPC } from '@trpc/server';
import superjson from 'superjson';
import { ZodError } from 'zod';

export type Context = inferAsyncReturnType<typeof createTRPCContext>;
const t = initTRPC.context<Context>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
  allowOutsideOfServer: true,
});

export const createTRPCRouter = t.router;

export const publicProcedure = t.procedure;
  1. 让我们设置我们的路由器,app/routes/api/root.ts
    P.S. 您可以创建不同的路由文件并将其添加到此处:
import { createTRPCRouter } from '~/routes/api/trpc';

export const appRouter = createTRPCRouter({
  testDelete: publicProcedure.input(z.object({
    id: XXXXXX
  })).mutation(async ({ ctx, input }) => {
    return ctx.prisma.test.delete({
      where: { id: input.id }
    })
  }),
});

// 导出 API 的类型定义
export type AppRouter = typeof appRouter;
  1. 创建服务器端 API 来使用路由器,将其放在与客户端侧文件中提到的相同路径中,例如在此示例中路径将是- app/routes/api/trpc/$.ts
import { createTRPCContext } from '~/routes/api/trpc';
import { appRouter } from '~/routes/api/root';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { ActionArgs, LoaderArgs } from '@remix-run/node';
// Action 和 Loaders 都将指向 tRPC 路由器
export const loader = async (args: LoaderArgs) => {
  return handleRequest(args);
};
export const action = async (args: ActionArgs) => {
  return handleRequest(args);
};
function handleRequest(args: LoaderArgs | ActionArgs) {
  return fetchRequestHandler({
    endpoint: '/api/trpc',
    req: args.request,
    router: appRouter,
    createContext: createTRPCContext,
  });
}

tRPC 设置现在已经完成,要使用它,请在任何组件中导入客户端并进行调用:

......
import { client } from '~/utils/api';
......
......
function onDelete(id) {
......
await client.testDelete.mutate({ id: id })
......
}

return <div>.....</div>;
英文:

Client Side:
This will be a common set-up for all frameworks, here I have used superjson as transformer for serialization-

// app/utils/api.ts
import {
  createTRPCProxyClient,
  httpLink,
  loggerLink,
} from &#39;@trpc/client&#39;;
import { type inferRouterInputs, type inferRouterOutputs } from &#39;@trpc/server&#39;;
import superjson from &#39;superjson&#39;;

import { type AppRouter } from &#39;~/routes/api/root&#39;;

const getBaseUrl = () =&gt; {
  if (typeof window !== &#39;undefined&#39;) return &#39;&#39;; // browser should use relative url
  // Change it to point to you SSR base URL
  return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
};


export const client = createTRPCProxyClient&lt;AppRouter&gt;({
  transformer: superjson,
  links: [
    loggerLink({
      enabled: (opts) =&gt;
        process.env.NODE_ENV === &#39;development&#39; ||
        (opts.direction === &#39;down&#39; &amp;&amp; opts.result instanceof Error),
    }),
    httpLink({
      url: `${getBaseUrl()}/api/trpc`, // We need to setup Server Side API to point to this
    }),
  ],
});


export type RouterInputs = inferRouterInputs&lt;AppRouter&gt;;

export type RouterOutputs = inferRouterOutputs&lt;AppRouter&gt;;

Server Side:

  1. Let's Setup the Procedure and Initialize Router app/routes/api/trpc.ts, here I am using Zod to intercept the request for type checking, you add Authentication layer also here:
import { FetchCreateContextFnOptions } from &#39;@trpc/server/adapters/fetch&#39;;
import { prisma } from &#39;~/utils/prisma.server&#39;;

type CreateContextOptions = Record&lt;string, never&gt;;

export const createTRPCContext = ({
  req,
  resHeaders,
}: FetchCreateContextFnOptions) =&gt; {
  return {
    prisma,
  };
};

import { inferAsyncReturnType, initTRPC } from &#39;@trpc/server&#39;;
import superjson from &#39;superjson&#39;;
import { ZodError } from &#39;zod&#39;;

export type Context = inferAsyncReturnType&lt;typeof createTRPCContext&gt;;
const t = initTRPC.context&lt;Context&gt;().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
  allowOutsideOfServer: true,
});

export const createTRPCRouter = t.router;

export const publicProcedure = t.procedure;

  1. Let's setup our Router, app/routes/api/root.ts:
    P.S. You can create different router files and add it here
import { createTRPCRouter } from &#39;~/routes/api/trpc&#39;;

export const appRouter = createTRPCRouter({
  testDelete: publicProcedure.input(z.object({
        id: XXXXXX
        })).mutation(async ({ctx, input}) =&gt; {
            return ctx.primsa.test.delete({
                where: { id: input.id }
            })
        }),

});

// export type definition of API
export type AppRouter = typeof appRouter;

  1. Create the Server Side API to consume the Routers, put it in the same path as mentioned in client side file, in this example path will be- app/routes/api/trpc/$.ts:
import { createTRPCContext } from &#39;~/routes/api/trpc&#39;;
import { appRouter } from &#39;~/routes/api/root&#39;;
import { fetchRequestHandler } from &#39;@trpc/server/adapters/fetch&#39;;
import { ActionArgs, LoaderArgs } from &#39;@remix-run/node&#39;;
// Both Action and Loaders will point to tRPC Router
export const loader = async (args: LoaderArgs) =&gt; {
 return handleRequest(args);
};
export const action = async (args: ActionArgs) =&gt; {
 return handleRequest(args);
};
function handleRequest(args: LoaderArgs | ActionArgs) {
 return fetchRequestHandler({
   endpoint: &#39;/api/trpc&#39;,
   req: args.request,
   router: appRouter,
   createContext: createTRPCContext,
 });
}

The tRPC Setup is now complete, to consume it import the client in any Component and make the call:

......
import { client } from &#39;~/utils/api&#39;;
......
......
function onDelete(id) {
......
await client.testDelete.mutate({ id: id })
......
}

return &lt;div&gt;.....&lt;/div&gt;


You can add more routes in appRouter accordingly.

huangapple
  • 本文由 发表于 2023年7月23日 17:10:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76747437.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定