How to incrementally adopt Edge functions with multiple TRPC backends

Photo by Jason Leung on Unsplash

How to incrementally adopt Edge functions with multiple TRPC backends

TRPC on Next.js Vercel Edge Functions

While researching edge functions for improving performance on my company's app lore.xyz, I found a way to get the client type safety of TRPC with the performance improvements of edge functions all bundled into a familiar trpc client API. But I needed a way to incrementally move some APIs over to the edge while maintaining the flexibly of traditional functions for existing APIs.

The trick revealed in this post is that you can have multiple trpc backends, one serving edge only traffic, and another serving traditional api traffic. I'll show you how to set this up yourself. Read on.

1 - Create a new TRPC router

// src/server/api/edge/routers/post.ts
import {createTRPCRouter} from "~/server/api/trpc";

export const routerEdge = createTRPCRouter({
    world: publicProcedure.query(({ctx}) => {
        return 'hello from edge router'
    }),
});

export const appRouterEdge = createTRPCRouter({
    hello: routerEdge,
});

2 - Create a TRPC Edge API Handler

This API handler is similar to the traditional TRPC handler except with a new path trpc-edge and using the fetch request handler adapter which is compatible with vercel's edge functions.

// src/pages/api/trpc-edge/[trpc].ts
import type { Post } from "@prisma/client";

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { NextRequest } from 'next/server';
import {appRouterEdge} from "../../../server/api/edge/edge-router-root";

export const config = {
    runtime: 'edge',
};

export default async function handler(req: NextRequest) {
    return fetchRequestHandler({
        endpoint: '/api/trpc-edge',
        router: appRouterEdge,
        req,
        createContext: () => ({}),
    });
}

3 - Integrate the edge router with your existing TRPC routes

Now you can update your existing TRPC router

// src/server/api/root.ts

// These are your existing routes.
export const nonEdgeRoutes = {
  myOtherRoutes: otherRoutes,
};

// We're adding all edge routes under the `edge.*` namespace.
// All other routes will still be accessible in with the same path.
export const appRouter = createTRPCRouter({
  edge: appRouterEdge,
  ...nonEdgeRoutes,
});


// export type definition of API to the client.
export type AppRouter = typeof appRouter;

4 - Route the client traffic to the correct trpc backend

// src/utils/api.ts

export const api = createTRPCNext<AppRouter>({
    config() {
        return {
            links: [
                //dynamic link to route requests to different servers (default api or edge api)
                (runtime) => {
                    // An API can route to the default server or the edge server
                    const servers = {
                        defaultServer: httpLink({
                            url: `${getBaseUrl()}/api/trpc`,
                        })(runtime),
                        edge: httpLink({
                            url: `${getBaseUrl()}/api/trpc-edge`,
                        })(runtime),
                    };
                    return (ctx) => {
                        const {op} = ctx;
                        // split the path by `.` as the first part will signify the server target name
                        const pathParts = op.path.split('.');

                        // edge routed apis will begin `edge.`
                        const serverName = pathParts[0];
                        if (serverName === 'edge') {
                            pathParts.shift();
                        }
                        const path = pathParts.join('.');
                        const link = serverName === 'edge' ? servers.edge : servers.defaultServer;
                        return link({
                            ...ctx,
                            op: {
                                ...op,
                                // override the target path with the prefix removed
                                path,
                            },
                        });
                    };
                },
            ],
        };
    },
    // ...rest of config
});

This is where the magic happens. By dynamically routing the client api call to the correct backend, the client doesn't need to know which backend is being called.

5 - Call your edge API

const MyComponent=()=>{
    const latestPost = api.edge.hello.world.useQuery();
    // ...rest of component
}

The edge APIs are merged with your existing non-edge API routes and can be called in the same familiar way. Under the hood, the edge.* apis are being routed to the edge function.

Code and expanded example

The example code below shows all of this in fully working detail as well as how to use Prisma/Kysley + Planetscale with this setup.

https://github.com/abigpotostew/trpc-edge-api-router