Raypx

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:

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 your Context type.
  • publicProcedure -- Alias for o. No authentication required.
  • protectedProcedure -- Uses requireAuth middleware. Throws UNAUTHORIZED if the user has no session.

Public Procedures

A public procedure has no middleware and is accessible to anyone:

packages/rpc/src/routers/index.ts
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:

packages/rpc/src/routers/profile.ts
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:

packages/rpc/src/routers/document.ts
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:

packages/rpc/src/routers/conversation.ts
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:

packages/rpc/src/routers/index.ts
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

On this page