reactjsprismaremixzodtrpc

How to Setup tRPC with Remix, Zod and Prisma


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.


Solution

  • 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 '@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 ''; // 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<AppRouter>({
      transformer: superjson,
      links: [
        loggerLink({
          enabled: (opts) =>
            process.env.NODE_ENV === 'development' ||
            (opts.direction === 'down' && opts.result instanceof Error),
        }),
        httpLink({
          url: `${getBaseUrl()}/api/trpc`, // We need to setup Server Side API to point to this
        }),
      ],
    });
    
    
    export type RouterInputs = inferRouterInputs<AppRouter>;
    
    export type RouterOutputs = inferRouterOutputs<AppRouter>;
    
    

    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 '@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. 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 '~/routes/api/trpc';
    
    export const appRouter = createTRPCRouter({
      testDelete: publicProcedure.input(z.object({
            id: XXXXXX
            })).mutation(async ({ctx, input}) => {
                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 '~/routes/api/trpc';
    import { appRouter } from '~/routes/api/root';
    import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
    import { ActionArgs, LoaderArgs } from '@remix-run/node';
    // Both Action and Loaders will point to tRPC Router
    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,
     });
    }
    
    

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

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

    You can add more routes in appRouter accordingly.