Create Next.JS client for tRPC server

PUBLISHED ON: Sunday, Mar 19, 2023

In the previous 🔗 article, we looked into setting up a tRPC API server. Now, let's look into using the API on the client side.

#

Create tRPC hooks for client side usage


// src/utils/trpc.ts

import { httpBatchLink, loggerLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { NextPageContext } from 'next';
import superjson from 'superjson';
import type { AppRouter } from '~/server/routers/_app';

/**
 * Here we define base url based on the different front-end services we plan to use
 */
export function getBaseUrl() {
  if (typeof window !== 'undefined') {
    return '';
  }

  return `http://localhost:${process.env.PORT ?? 3000}`;
}

/**
 * Extend `NextPageContext` with meta data that can be picked up by `responseMeta()` when server-side rendering
 */
export interface SSRContext extends NextPageContext {
  status?: number;
}

/**
 * A set of strongly-typed React hooks from our `AppRouter` type signature with `createReactQueryHooks`
 */
export const trpc = createTRPCNext<AppRouter, SSRContext>({
  config({ ctx }) {
    return {
      transformer: superjson,
      links: [
        // adds pretty logs to the console in development and logs errors in production
        loggerLink({
          enabled: (opts) =>
            process.env.NODE_ENV === 'development' ||
            (opts.direction === 'down' && opts.result instanceof Error),
        }),
        httpBatchLink({
          // If we want to use SSR, we need to use the server's full URL
          url: `${getBaseUrl()}/api/trpc`,
          // Set custom request headers on every request from tRPC
          headers() {
            if (ctx?.req) {
              // To use SSR properly, we need to forward the client's headers to the server
              // This is so that we can pass through things like cookies when we're server-side rendering

              // When using Node 18, omit the "connection" header
              const {
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
                connection: _connection,
                ...headers
              } = ctx.req.headers;
              return {
                ...headers,
                // Optional: inform server that it's an SSR request
                'x-ssr': '1',
              };
            }
            return {};
          },
        }),
      ],
      queryClientConfig: {
        defaultOptions: {
          queries: {
            staleTime: 60,
          },
        },
      },
    };
  },
  // client is not going to see the requests in the networks tab
  ssr: true,
  /**
   * Set headers or status code when doing SSR
   */
  responseMeta(opts) {
    const ctx = opts.ctx as SSRContext;

    if (ctx.status) {
      // If HTTP status set, propagate that
      return {
        status: ctx.status,
      };
    }

    const error = opts.clientErrors[0];
    if (error) {
      // Propagate http first error from API calls
      return {
        status: error.data?.httpStatus ?? 500,
      };
    }

    return {};
  },
});

export type RouterInput = inferRouterInputs<AppRouter>;
export type RouterOutput = inferRouterOutputs<AppRouter>;

#

Setup tRPC HTTP response handler


// src/pages/api/trpc/[trpc].ts

import { createNextApiHandler } from '@trpc/server/adapters/next';
import { createContext } from '~/server/context';
import { appRouter } from '~/server/routers/_app';

export default createNextApiHandler({
  router: appRouter,
  createContext,
  onError({ error }) {
    if (error.code === 'INTERNAL_SERVER_ERROR') {
      console.error('Something went wrong', error);
    } else {
      console.log({ error });
    }
  },
  batching: {
    enabled: true,
  },
});

Now we are ready to use tRPC in React components.

#

Create React context for user


import { createContext, ReactNode, useContext } from 'react';
import { RouterOutput } from '~/utils/trpc';

const UserContext = createContext<RouterOutput['users']['me']>(null);

function UserContextProvider({
  children,
  value,
}: {
  children: ReactNode;
  value: RouterOutput['users']['me'] | undefined;
}) {
  return (
    <UserContext.Provider value={value || null}>
      {children}
    </UserContext.Provider>
  );
}

const useUserContext = () => useContext(UserContext);

export { useUserContext, UserContextProvider };

#

Login flow

The login flow would consist of:

  • Login user
  • Request OTP
  • Navigate to verify OTP screen

import { trpc } from '~/utils/trpc';

const requestOtp = trpc.users['request-otp'].useMutation({
    onSuccess() {
      router.push('/verify-otp');
    },
  });

const login = trpc.users['login'].useMutation({
  onSuccess() {
    // Request OTP
    requestOtp.mutate({
      email: methods.getValues('email'),
    });
  },
});

function onSubmit(values: LoginUserInput) {
  login.mutate({
    ...values,
  });
}

#

Registration flow


const requestOtp = trpc.users['request-otp'].useMutation({
  onSuccess() {
    router.push('/verify-otp');
  },
});

// error.message can be used to display the message in case of error
const { mutate, error } = trpc.users['register-user'].useMutation({
  onSuccess() {
    requestOtp.mutate({
      email: methods.getValues('email'),
    });
  },
});

/**
 * export type CreateUserInput = TypeOf<typeof registerUserSchema>;
 * For more info,
 * @link https://manojp1991.dev/article/2023/create-a-trpc-api
 */
function onSubmit(values: CreateUserInput) {
  mutate(values);
}

#

Verify OTP screen


const { data, mutate, error } = trpc.users['verify-otp'].useMutation({
  onSuccess() {
    router.replace(data?.redirect || '/');
  },
});

function onSubmit(values: VerifyOtpInput) {
  mutate({
    ...values,
  });
}

#

Create a post


const { mutate, error } = trpc.posts['add'].useMutation({
  onSuccess({ id }) {
    router.push(`/posts/${id}`);
  },
});

/**
 * export type CreatePostInput = z.infer<typeof createPostSchema>;
 */
function onSubmit(values: CreatePostInput) {
  mutate(values);
}

#

List posts screen


const postsQuery = trpc.posts['list'].useInfiniteQuery(
  {
    limit: 5,
  },
  {
    getPreviousPageParam(lastPage) {
      return lastPage.nextCursor;
    },
  },
);

#

Get post by id


const { data, error, isLoading } = trpc.posts.byId.useQuery({ id });

#

Reference

🔗 Repository