Skip to content
GitHub Twitter

Run tRPC on the edge with Next.js

By default the T3 Stack sets you up with a working tRPC API - which is great - aside from the lambda cold starts... Luckily, tRPC is also able to run on Vercel's new speedy edge network. Provided the backend code you run within your tRPC API is also able to run on the edge (Prisma is a notable exclusion here), you will see a marked improvement in response times and reduction in cold starts by swapping over.

Background on tRPC and Adapters

tRPC is a great way to structure your backend to have full-stack typesafety. However, it is not a backend on its own and therefore must be served from a host - a http server, Express, Next.js API routes, Cloudflare workers - the list goes on. Adapters facilitate the communication between the host server and the tRPC API.

Update Catch-all API Handler

The T3 Starter repo runs on Next.js, and provides you with an adapter in the form of a catch-all Next.js API handler:

import { createNextApiHandler } from "@trpc/server/adapters/next";
import { env } from "~/env.mjs";
import { createTRPCContext } from "~/server/api/trpc";
import { appRouter } from "~/server/api/root";

// export API handler
export default createNextApiHandler({
  router: appRouter,
  createContext: createTRPCContext,
  onError:
    env.NODE_ENV === "development"
      ? ({ path, error }) => {
          console.error(
            `❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
          );
        }
      : undefined,
});

This runs on the Next.js serverless network (on a lambda function), and hence is prone to slow cold starts.

To convert this to a Next.js Edge function, the adapter needs to be updated to use the fetchRequestAdapter, as well as an exported edge config:

import { createTRPCContext } from "~/server/api/trpc";
import { appRouter } from "~/server/api/root";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { type NextRequest } from "next/server";

export const config = {
  runtime: "edge",
  region: "fra1",
};

export default async function handler(req: NextRequest) {
  return fetchRequestHandler({
    endpoint: "/api/trpc",
    router: appRouter,
    req,
    createContext: createTRPCContext,
  });
}

We also type the incoming request req: NextRequest to extend the native fetch Request interface with the additional edge-related properties provided by Next.js. We then edit the tRPC Context as below.

Update tRPC Context

The tRPC context is where you can add any additional data to your tRPC API. In the T3 Starter, we use this to add the prisma client to the context, so that we can access it within our tRPC API without having to create e new reference to it in each route.

import { prisma } from "~/server/db";

type CreateContextOptions = Record<string, never>;

const createInnerTRPCContext = (_opts: CreateContextOptions) => {
  return {
    prisma,
  };
};

export const createTRPCContext = (_opts: CreateNextContextOptions) => {
  return createInnerTRPCContext({});
};

tRPC provides helpful types for the CreateContext function while running on the edge in the form of FetchCreateContextFnOptions. As prisma is not able to run on the edge, we must also remove it from the (inner) context.

import { type inferAsyncReturnType, initTRPC } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { type FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
type CreateContextOptions = {
  req: Request,
};

const createInnerTRPCContext = ({ req }: CreateContextOptions) => {
  return {};
};

export const createTRPCContext = ({ req }: FetchCreateContextFnOptions) => {
  return createInnerTRPCContext({ req });
};

export type Context = inferAsyncReturnType<typeof createTRPCContext>;

We also add this exported Context type as a generic to initialize the tRPC API and we are done:


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,
      },
    };
  },
});

Conclusion

I know there is a bit of config to wrestle with there, but hopefully this post clarifies a few things and gets you up and running on the edge with tRPC. I was surprised that there was no existing documentation to guide users through the process end-to-end - I would have saved a lot of time if there were!

The source code can be found here