Fetch / Edge Runtimes Adapter
You can create a tRPC server within any edge runtime that follow the WinterCG, specifically the Minimum Common Web Platform API specification.
Some of these runtimes include, but are not limited to:
- Cloudflare Workers
- Deno Deploy
- Vercel Edge Runtime (& Next.js Edge Runtime)
This also makes it easy to integrate into frameworks that use the web platform APIs to represent requests and responses, such as:
- Astro (SSR mode)
- Remix
- SolidStart
Example apps
| Description | Links |
|---|---|
| Cloudflare Workers example | Source |
| Deno Deploy example | Source |
| Next.js Edge Runtime example | Source |
| Vercel Edge Runtime example | Source |
How to use tRPC server with an edge runtime
tRPC provides a fetch adapter that uses the native Request and Response APIs as input and output. The tRPC-specific code is the same across all runtimes, the only difference being how the response is returned.
tRPC includes an adapter for the native Fetch API out of the box. This adapter lets you convert your tRPC router into a Request handler that returns Response objects.
Required Web APIs
tRPC server uses the following Fetch APIs:
Request,ResponsefetchHeadersURL
If your runtime supports these APIs, you can use tRPC server.
Fun fact: that also means you can use a tRPC server in your browser!
Common setup
Install dependencies
You can skip this step if you use Deno Deploy.
- npm
- yarn
- pnpm
- bun
- deno
npm install @trpc/server @trpc/client zod
yarn add @trpc/server @trpc/client zod
pnpm add @trpc/server @trpc/client zod
bun add @trpc/server @trpc/client zod
deno add npm:@trpc/server npm:@trpc/client npm:zod
Zod isn't a required dependency, but it's used in the sample router below.
Create the router
First of all you need a router to handle your queries, mutations and subscriptions.
A sample router is given below, save it in a file named router.ts.
router.ts
router.tstsimport {initTRPC } from '@trpc/server';import {z } from 'zod';import type {Context } from './context';typeUser = {id : string;name : string;bio ?: string;};constusers :Record <string,User > = {};export constt =initTRPC .context <Context >().create ();export constappRouter =t .router ({getUserById :t .procedure .input (z .string ()).query ((opts ) => {returnusers [opts .input ]; // input type is string}),createUser :t .procedure // validate input with Zod.input (z .object ({name :z .string ().min (3),bio :z .string ().max (142).optional (),}),).mutation ((opts ) => {constid =Date .now ().toString ();constuser :User = {id , ...opts .input };users [user .id ] =user ;returnuser ;}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
router.tstsimport {initTRPC } from '@trpc/server';import {z } from 'zod';import type {Context } from './context';typeUser = {id : string;name : string;bio ?: string;};constusers :Record <string,User > = {};export constt =initTRPC .context <Context >().create ();export constappRouter =t .router ({getUserById :t .procedure .input (z .string ()).query ((opts ) => {returnusers [opts .input ]; // input type is string}),createUser :t .procedure // validate input with Zod.input (z .object ({name :z .string ().min (3),bio :z .string ().max (142).optional (),}),).mutation ((opts ) => {constid =Date .now ().toString ();constuser :User = {id , ...opts .input };users [user .id ] =user ;returnuser ;}),});// export type definition of APIexport typeAppRouter = typeofappRouter ;
If your router file starts getting too big, split your router into several subrouters each implemented in its own file. Then merge them into a single root appRouter.
Create the context
Then you need a context that will be created for each request.
A sample context is given below, save it in a file named context.ts:
context.ts
context.tstsimport type {FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';export functioncreateContext ({req ,resHeaders ,}:FetchCreateContextFnOptions ) {constuser = {name :req .headers .get ('username') ?? 'anonymous' };return {req ,resHeaders ,user };}export typeContext =Awaited <ReturnType <typeofcreateContext >>;
context.tstsimport type {FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';export functioncreateContext ({req ,resHeaders ,}:FetchCreateContextFnOptions ) {constuser = {name :req .headers .get ('username') ?? 'anonymous' };return {req ,resHeaders ,user };}export typeContext =Awaited <ReturnType <typeofcreateContext >>;
Runtime-specific setup
Astro
src/pages/trpc/[trpc].tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import type {APIRoute } from 'astro';import {createContext } from '../../server/context';import {appRouter } from '../../server/router';export constALL :APIRoute = (opts ) => {returnfetchRequestHandler ({endpoint : '/trpc',req :opts .request ,router :appRouter ,createContext ,});};
src/pages/trpc/[trpc].tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import type {APIRoute } from 'astro';import {createContext } from '../../server/context';import {appRouter } from '../../server/router';export constALL :APIRoute = (opts ) => {returnfetchRequestHandler ({endpoint : '/trpc',req :opts .request ,router :appRouter ,createContext ,});};
Cloudflare Worker
You need the Wrangler CLI to run Cloudflare Workers.
Create Cloudflare Worker
server.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createContext } from './context';import {appRouter } from './router';export default {asyncfetch (request :Request ):Promise <Response > {returnfetchRequestHandler ({endpoint : '/trpc',req :request ,router :appRouter ,createContext ,});},};
server.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createContext } from './context';import {appRouter } from './router';export default {asyncfetch (request :Request ):Promise <Response > {returnfetchRequestHandler ({endpoint : '/trpc',req :request ,router :appRouter ,createContext ,});},};
Run wrangler dev server.ts and your endpoints will be available via HTTP!
| Endpoint | HTTP URI |
|---|---|
getUser | GET http://localhost:8787/trpc/getUserById?input=INPUT where INPUT is a URI-encoded JSON string. |
createUser | POST http://localhost:8787/trpc/createUser with req.body of type User |
Deno Oak
This assumes you have Deno installed and setup. Refer to their getting started guide for more information.
Update the imports in router.ts
router.tstsimport { initTRPC } from 'npm:@trpc/server';import { z } from 'npm:zod';import { Context } from './context.ts';
router.tstsimport { initTRPC } from 'npm:@trpc/server';import { z } from 'npm:zod';import { Context } from './context.ts';
Update the imports in context.ts
context.tstsimport { FetchCreateContextFnOptions } from 'npm:@trpc/server/adapters/fetch';
context.tstsimport { FetchCreateContextFnOptions } from 'npm:@trpc/server/adapters/fetch';
Use fetchRequestHandler with Oak in app.ts
app.tstsimport { Application, Router } from 'https://deno.land/x/oak/mod.ts';import { fetchRequestHandler } from 'npm:@trpc/server/adapters/fetch';import { createContext } from './context.ts';import { appRouter } from './router.ts';const app = new Application();const router = new Router();router.all('/trpc/(.*)', async (ctx) => {const res = await fetchRequestHandler({endpoint: '/trpc',req: new Request(ctx.request.url, {headers: ctx.request.headers,body:ctx.request.method !== 'GET' && ctx.request.method !== 'HEAD'? ctx.request.body({ type: 'stream' }).value: void 0,method: ctx.request.method,}),router: appRouter,createContext,});ctx.response.status = res.status;ctx.response.headers = res.headers;ctx.response.body = res.body;});app.use(router.routes());app.use(router.allowedMethods());await app.listen({ port: 3000 });
app.tstsimport { Application, Router } from 'https://deno.land/x/oak/mod.ts';import { fetchRequestHandler } from 'npm:@trpc/server/adapters/fetch';import { createContext } from './context.ts';import { appRouter } from './router.ts';const app = new Application();const router = new Router();router.all('/trpc/(.*)', async (ctx) => {const res = await fetchRequestHandler({endpoint: '/trpc',req: new Request(ctx.request.url, {headers: ctx.request.headers,body:ctx.request.method !== 'GET' && ctx.request.method !== 'HEAD'? ctx.request.body({ type: 'stream' }).value: void 0,method: ctx.request.method,}),router: appRouter,createContext,});ctx.response.status = res.status;ctx.response.headers = res.headers;ctx.response.body = res.body;});app.use(router.routes());app.use(router.allowedMethods());await app.listen({ port: 3000 });
Deno Deploy
This assumes you have Deno installed and setup. Refer to their getting started guide for more information.
See our example Deno Deploy app for a working example.
Update the imports in router.ts
router.tstsimport { initTRPC } from 'npm:@trpc/server';import { z } from 'npm:zod';import { Context } from './context.ts';
router.tstsimport { initTRPC } from 'npm:@trpc/server';import { z } from 'npm:zod';import { Context } from './context.ts';
Update the imports in context.ts
context.tstsimport { FetchCreateContextFnOptions } from 'npm:@trpc/server/adapters/fetch';
context.tstsimport { FetchCreateContextFnOptions } from 'npm:@trpc/server/adapters/fetch';
Create Deno Deploy Function
server.tstsimport { fetchRequestHandler } from 'npm:@trpc/server/adapters/fetch';import { createContext } from './context.ts';import { appRouter } from './router.ts';function handler(request) {return fetchRequestHandler({endpoint: '/trpc',req: request,router: appRouter,createContext,});}Deno.serve(handler);
server.tstsimport { fetchRequestHandler } from 'npm:@trpc/server/adapters/fetch';import { createContext } from './context.ts';import { appRouter } from './router.ts';function handler(request) {return fetchRequestHandler({endpoint: '/trpc',req: request,router: appRouter,createContext,});}Deno.serve(handler);
Run deno run --allow-net=:8000 --allow-env ./server.ts and your endpoints will be available via HTTP!
| Endpoint | HTTP URI |
|---|---|
getUser | GET http://localhost:8000/trpc/getUserById?input=INPUT where INPUT is a URI-encoded JSON string. |
createUser | POST http://localhost:8000/trpc/createUser with req.body of type User |
Next.js Edge Runtime
See a full example here.
Remix
app/routes/trpc.$trpc.tstsimport type {ActionFunctionArgs ,LoaderFunctionArgs } from '@remix-run/node';import {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createContext } from '~/server/context';import {appRouter } from '~/server/router';export constloader = async (args :LoaderFunctionArgs ) => {returnhandleRequest (args );};export constaction = async (args :ActionFunctionArgs ) => {returnhandleRequest (args );};functionhandleRequest (args :LoaderFunctionArgs |ActionFunctionArgs ) {returnfetchRequestHandler ({endpoint : '/trpc',req :args .request ,router :appRouter ,createContext ,});}
app/routes/trpc.$trpc.tstsimport type {ActionFunctionArgs ,LoaderFunctionArgs } from '@remix-run/node';import {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createContext } from '~/server/context';import {appRouter } from '~/server/router';export constloader = async (args :LoaderFunctionArgs ) => {returnhandleRequest (args );};export constaction = async (args :ActionFunctionArgs ) => {returnhandleRequest (args );};functionhandleRequest (args :LoaderFunctionArgs |ActionFunctionArgs ) {returnfetchRequestHandler ({endpoint : '/trpc',req :args .request ,router :appRouter ,createContext ,});}
SolidStart
src/routes/api/trpc/[trpc].tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import type {APIEvent } from '@solidjs/start/server';import {createContext } from '../../server/context';import {appRouter } from '../../server/router';consthandler = (event :APIEvent ) =>fetchRequestHandler ({endpoint : '/api/trpc',req :event .request ,router :appRouter ,createContext ,});export {handler asGET ,handler asPOST };
src/routes/api/trpc/[trpc].tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import type {APIEvent } from '@solidjs/start/server';import {createContext } from '../../server/context';import {appRouter } from '../../server/router';consthandler = (event :APIEvent ) =>fetchRequestHandler ({endpoint : '/api/trpc',req :event .request ,router :appRouter ,createContext ,});export {handler asGET ,handler asPOST };
Vercel Edge Runtime
See the official Vercel Edge Runtime documentation for more information.
See our example Vercel Edge Runtime app for a working example.
Install dependencies
- npm
- yarn
- pnpm
- bun
shnpm install -g edge-runtime
shnpm install -g edge-runtime
shyarn global add edge-runtime
shyarn global add edge-runtime
shpnpm add -g edge-runtime
shpnpm add -g edge-runtime
shbun add -g edge-runtime
shbun add -g edge-runtime
Create Edge Runtime Function
server.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createContext } from './context';import {appRouter } from './router';// Vercel Edge Runtime uses Service Worker-style addEventListeneraddEventListener ('fetch', (event : any) => {returnevent .respondWith (fetchRequestHandler ({endpoint : '/trpc',req :event .request ,router :appRouter ,createContext ,}),);});
server.tstsimport {fetchRequestHandler } from '@trpc/server/adapters/fetch';import {createContext } from './context';import {appRouter } from './router';// Vercel Edge Runtime uses Service Worker-style addEventListeneraddEventListener ('fetch', (event : any) => {returnevent .respondWith (fetchRequestHandler ({endpoint : '/trpc',req :event .request ,router :appRouter ,createContext ,}),);});
Run edge-runtime --listen server.ts --port 3000 and your endpoints will be available via HTTP!
| Endpoint | HTTP URI |
|---|---|
getUser | GET http://localhost:3000/trpc/getUserById?input=INPUT where INPUT is a URI-encoded JSON string. |
createUser | POST http://localhost:3000/trpc/createUser with req.body of type User |