Skip to Content
Docs
Serverless
RPC Functions

RPC Functions

Serverless functions are TypeScript HTTP routes that allow you to build REST endpoints.

To get started:

  • You’ll need to create a server/routes/ folder
  • You’ll also need to install zod via yarn add zod. Zod is used for validation of RPC inputs.

The server/routes folder contains a set of .ts files, and uses file-based routing. You can also create an optional server/_context.ts file, which allows you to parse HTTP headers for things like auth, and pass additional information to your API handlers.

Each file must have a default export, and export either a query, mutation, or a router.

You can also add additional helper code anywhere in the server folder, and it’ll be ignored — as long as it’s not in the server/routes folder. Of course, you can also import code from elsewhere in your project.

An example folder structure:

    • _context.ts
      • api.pokemon.ts
      • api.echo.ts
          • auth.ts
          • prefs.ts
      • whatever.ts

Under the hood, the stack uses tRPC  to define server-side procedures and routers — with custom a integration and implementation. You should familiarize yourself with the core concepts of procedures, routing and context in tRPC, but examples are shown on this page. You don’t need to install tRPC, as it’s included in the stack.

RPC functions require a Vercel deployment. The useRPCQuery() and useRPCMutation() hooks will work correctly in WordPress admin and the SPA frontend, as long as the serverless.endpoints object in ed.config.json is configured correctly — it must contain a mapping of the WordPress origin to the Serverless endpoint, for example "cms.ed.studio": "ed.studio".

Defining Routes

Routes are defined using file-based routing. You can use a combination of folders and filenames with . so api/hello.ts and api.hello.ts do the same thing! It’s recommended that you use api.hello.ts so that your filenames are easier to search for and recognise in your code editor.

Below are some examples of how the file-based routing is resolved:

FileURL PathNotes

/routes/demo1.ts

/demo1

Straight-up file -> URL

/routes/api/tickets.ts

/api/tickets/

Using folders

/routes/api.tickets.ts

/api/tickets/

Using filenames only with . instead of folders

/routes/api/hubspot.forms.ts

/api/hubspot/forms/

Using a combination of folders + . syntax

Procedures

“Procedures” are either “queries” or “mutations”. Queries are GET requests, and mutations are POST requests. Both have a nearly identical syntax, but are called in different ways.

Multiple procedures can be contained within a single file by exporting a “router”.

Queries

Query procedures use the GET HTTP method.

/server/routes/api.getPokemon.ts
import { rpc } from "eddev/server" import { z } from "zod" export default rpc.procedure .input( z.object({ name: z.string().min(2), }), ) .query(async ({ ctx, input }) => { const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${input.name}`) const data = await response.json() return { pokemonInfo: data, } })

useRPCQuery()

In React, you can then call useRPCQuery with the name "api.getPokemon" and type-safe arguments, and observe the result.

PokemonInfo.tsx
import { useRPCQuery } from "eddev/hooks" type Props = { name: string } export function Pokemon(props: Props) { const pokemon = useRPCQuery("api.getPokemon", { name: props.name }) if (pokemon.isSuccess) { return ( <div> Pokemon data: <pre>{JSON.stringify(pokemon.data.pokemonInfo, null, 2)}</pre> </div> ) } else if (pokemon.isLoading) { return <div>Loading...</div> } else if (pokemon.isError) { return <div>Error: {pokemon.error.message}</div> } }

The return result of useRPCQuery is the same as our generated GraphQL hooks. It also accepts a third argument, the query options, where you can pass options like refetchInterval to have the hook auto-refresh. See TanStack docs for useQuery 

rpcClient.query()

You can also call queries procedurally using rpcClient.query(), like so:

const bulbasaur = await rpcClient.query("api.getPokemon", { name: "bulbasaur" }) console.log("Bulbasaur is", bulbasaur)

Mutations

Use mutation procedures for actions that should be triggered when a user completes an action, like submitting a form.

Mutations will be called from React with const mutation = useRPCMutation("api.submit") and mutation.mutate(args)

/server/routes/api.subscribe.ts
import { rpc } from "eddev/server" import { z } from "zod" export default rpc.procedure .input( z.object({ email: z.string(), listId: z.string() }), ) .mutation(async ({ input }) => { const params = new URLSearchParams() params.append("EMAIL", input.email) params.append("id", item.listId) const response = fetch(`${env.CAMPAIGN_MONITOR_URL}&${params}`, { method: "get", }) if (response.ok) { return { success: true } } else { return { success: false } } })

useRPCMutation()

In React, you can then call useRPCQuery with the name "api.getPokemon" and type-safe arguments, and observe the result.

PokemonInfo.tsx
import { useRPCQuery } from "eddev/hooks" type Props = { listId: string } export function SignupForm(props: Props) { const subscribe = useRPCMutation("api.subscribe") if (subscribe.isSuccess) { console.log("Result is", subscribe.data) return <div>Thank you for signing up!</div> } return <form onSubmit={e => { e.preventDefault() submit.mutate({ email: email, listId: props.listId }) }}> {/* Error message */} {subscribe.isError && <p>{subscribe.error.message}</p>} {/* Disable input when submitting */} <input type="email" disabled={subscribe.isSubmitting} value={email} onChange={e => setEmail(e.currentTarget.value)} /> <button type="submit">Submit</button> </form> }

The return result of useRPCMutation is the same as our generated GraphQL hooks. It also accepts a second argument, the query options, where you can pass options like onSuccess to trigger functions to occur after successful submission. See TanStack docs for useQuery 

rpcClient.mutation()

You can also call queries procedurally using rpcClient.query(), like so:

const result = await rpcClient.mutation("api.subscribe", { email: "hello@internet.com", listId: "12345" }) console.log("Sign up result", result)

Routers

Multiple procedures can be exported from a single file by exporting a “router”. A router mounts multiple endpoints at once. Use rpc.router() when you have a group of highly-related API functions.

/server/routes/api.math.ts
import { rpc } from "eddev/server" import z from "zod" export default rpc.router({ sum: rpc.procedure .input( z.object({ a: z.number(), b: z.number(), }), ) .query(({ input }) => { return input.a + input.b }), random: rpc.procedure.query(() => { return Math.random() }), pi: rpc.procedure.query(() => { return Math.PI }), })

Procedures defined in a router can be called using useRPCQuery() and useRPCMutation(), like any other procedure.

import { useRPCQuery } from "eddev/hooks" export function Demo() { const pi = useRPCQuery("api.math.pi") const random = useRPCQuery("api.math.random") const sum = useRPCQuery( "api.math.sum", { a: pi.data ?? 0, b: random.data ?? 0 }, { enabled: pi.isSuccess && random.isSuccess, }, ) return ( <div> <div>Pi: {pi.data}</div> <div>Random: {random.data}</div> <div>Sum: {sum.data}</div> </div> ) }

Context Middleware

The /server/_context.ts file is optional, but can be defined to extract common metadata from a request. You can use it to parse Auth headers or cookies, and the returned values will be available in the ctx argument of your procedures.

/server/_context.ts
import { defineServerContext } from "eddev/server" export default defineServerContext((ctx) => { const headers = ctx.req.headers return { ...ctx, user: { ip: headers.get("x-real-ip") ?? headers.get("x-real-ip") ?? "", }, } })

You’ll need to run eddev dev to ensure that the TypeScript types are generated at least once. In any procedure, you can now access ctx.user.ip.

/server/api.ip.ts
import { rpc } from "eddev/server" export default rpc.procedure.query(async ({ ctx }) => { return { ip: "Your IP is " + ctx.user.ip, } })

Connecting to WordPress

Generated GraphQL hooks can be called using .fetch() and .mutate(), with arguments.

import { rpc } from "eddev/server" export default rpc.procedure .query(async ({ ctx, input }) => { const data = useLatestNews.fetch({ category: "design" }) return { latest: data.news.nodes[0] } })

Non-RPC Responses

RPC queries typically return JSON payloads with a Content-Type: application/json header, however procedures can also be used as generic HTTP endpoints, by simply returning a native JavaScript Response object. Note that this will only work on Vercel-deployed domains.

import { rpc } from "eddev/server" export default rpc.procedure.query(async () => { return new Response("Hello world", { status: 200, headers: { "Content-Type": "text/plain", "Cache-Control": "max-age=3600, s-maxage=3600, public", }, }) })

Note that when returning a Response object, the procedure may no longer be callable with useRPCQuery() and useRPCMutation(), since it may no longer conform to tRPC’s return protocol.

There are also some helper methods for force-rendering pages from other routes.

The renderPage() function can be used to display a page with a specific route. This should be avoided in the majority of cases.

import { rpc, renderPage } from "eddev/server" export default rpc.procedure.query(async () => { return renderPage({ pathname: "/", }) })

Similarly, the renderErrorPage() function can be used to display the site’s standard error page.

import { renderErrorPage, rpc } from "eddev/server" export default rpc.procedure.query(async () => { const result = await renderErrorPage({ title: "Bad Request", statusCode: 400, userMessage: "Something went wrong", }) return result })
Last updated on