Defining Procedures
How to create public and protected RPC procedures.
oRPC procedures are the building blocks of the Raypx API. Each procedure defines input validation, middleware, and a handler function. Procedures are organized into routers for grouping related functionality.
The o Base
All procedures start from the o instance defined in packages/rpc/src/index.ts:
import { ORPCError, os } from "@orpc/server"
import type { Context } from "./context"
export const o = os.$context<Context>()
export const publicProcedure = o
const requireAuth = o.middleware(async ({ context, next }) => {
if (!context.session?.user) {
throw new ORPCError("UNAUTHORIZED")
}
return next({
context: {
session: context.session,
},
})
})
export const protectedProcedure = publicProcedure.use(requireAuth)o-- The base oRPC instance bound to yourContexttype.publicProcedure-- Alias foro. No authentication required.protectedProcedure-- UsesrequireAuthmiddleware. ThrowsUNAUTHORIZEDif the user has no session.
Public Procedures
A public procedure has no middleware and is accessible to anyone:
export const appRouter = {
healthCheck: publicProcedure.handler(async () => {
const db = createDb()
const [database] = await checkService(() => db.execute(sql`SELECT 1`))
return { status: database.status, timestamp: Date.now(), database }
}),
}Protected Procedures
Protected procedures require a valid session. The context.session is guaranteed to exist after the requireAuth middleware:
export const profileRouter = {
get: protectedProcedure.handler(async ({ context }) => {
const db = createDb()
const [profile] = await db
.select()
.from(profiles)
.where(eq(profiles.userId, context.session.user.id))
.limit(1)
return profile ?? null
}),
}Procedures with Input
Use .input() to validate incoming data:
export const documentRouter = {
get: protectedProcedure
.input(z.object({ id: z.string().min(1) }))
.handler(async ({ context, input }) => {
const db = createDb()
const [doc] = await db
.select()
.from(documents)
.where(and(eq(documents.id, input.id), eq(documents.userId, context.session.user.id)))
.limit(1)
if (!doc) throw new Error("Document not found")
return doc
}),
}Organizing Procedures into Routers
Group related procedures into router objects:
export const conversationRouter = {
list: protectedProcedure.handler(async ({ context }) => { /* ... */ }),
get: protectedProcedure.input(z.object({ id: z.string().min(1) })).handler(async ({ context, input }) => { /* ... */ }),
create: protectedProcedure.input(z.object({ title: z.string().min(1) })).handler(async ({ context, input }) => { /* ... */ }),
sendMessage: protectedProcedure.input(z.object({
conversationId: z.string().min(1),
content: z.string().min(1),
})).handler(async ({ context, input }) => { /* ... */ }),
}Then mount the router on the appRouter:
export const appRouter = {
healthCheck: publicProcedure.handler(/* ... */),
profile: profileRouter,
documents: documentRouter,
conversations: conversationRouter,
usage: usageRouter,
}Adding a New Router
Create a new file in packages/rpc/src/routers/, for example my-feature.ts. Define a router object with procedures.
Import the router in packages/rpc/src/routers/index.ts and add it to appRouter.
The oRPC client infers types from appRouter automatically. Call client.myFeature.myProcedure.query() or .mutate() from your React components.
Example of adding a router:
// packages/rpc/src/routers/my-feature.ts
import { protectedProcedure } from "../index"
export const myFeatureRouter = {
doSomething: protectedProcedure.handler(async ({ context }) => {
return { message: `Hello, ${context.session.user.name}` }
}),
}// packages/rpc/src/routers/index.ts
import { myFeatureRouter } from "./my-feature"
export const appRouter = {
// ...existing routers
myFeature: myFeatureRouter,
}The client now has full type safety:
// Type-safe client call
const result = await client.myFeature.doSomething.query()
// result.message is typed as string