Features
API Layer
Build type-safe APIs with ORPC for end-to-end type safety.
Overview
Raypx uses ORPC for building type-safe APIs. ORPC provides a tRPC-like development experience with a simpler architecture, giving you end-to-end type safety from server to client.
Features
- End-to-End Type Safety: Full TypeScript inference from server to client
- Minimal Bundle Size: Lightweight compared to alternatives
- Server-Side Focus: Designed for server-first architectures
- React Integration: Built-in hooks for data fetching
- Middleware Support: Composable middleware for auth, logging, etc.
Project Structure
packages/rpc/
├── src/
│ ├── context.ts # Request context types
│ ├── middleware/ # Middleware (auth, logging)
│ ├── procedures/ # API procedures
│ ├── routers/ # Route definitions
│ └── client.ts # Client exports
└── index.tsDefining Procedures
Basic Procedure
// packages/rpc/procedures/user.ts
import { o } from "@orpc/server";
import { publicProcedure } from "../trpc";
export const getUser = publicProcedure
.input(o.object({ id: o.string() }))
.handler(async ({ input, context }) => {
const user = await context.db.user.findById(input.id);
if (!user) {
throw new Error("User not found");
}
return user;
});Protected Procedure
// packages/rpc/procedures/user.ts
import { protectedProcedure } from "../trpc";
export const getProfile = protectedProcedure
.handler(async ({ context }) => {
return context.user;
});
export const updateProfile = protectedProcedure
.input(o.object({
name: o.string().optional(),
email: o.string().email().optional(),
}))
.handler(async ({ input, context }) => {
const updated = await context.db.user.update(context.user.id, input);
return updated;
});With Validation
import { o } from "@orpc/server";
import { protectedProcedure } from "../trpc";
export const createPost = protectedProcedure
.input(o.object({
title: o.string().min(1).max(200),
content: o.string().min(1),
tags: o.array(o.string()).optional(),
published: o.boolean().default(false),
}))
.handler(async ({ input, context }) => {
const post = await context.db.post.create({
...input,
authorId: context.user.id,
});
return post;
});Creating Routers
Define Router
// packages/rpc/routers/index.ts
import { router } from "@orpc/server";
import * as userProcedures from "../procedures/user";
import * as postProcedures from "../procedures/post";
export const appRouter = router({
user: {
get: userProcedures.getUser,
profile: userProcedures.getProfile,
update: userProcedures.updateProfile,
},
post: {
list: postProcedures.listPosts,
create: postProcedures.createPost,
delete: postProcedures.deletePost,
},
});
export type AppRouter = typeof appRouter;Server Setup
// apps/web/src/routes/api/rpc/[...catchall].ts
import { createAPIFileRoute } from "@tanstack/react-start";
import { appRouter } from "@raypx/rpc";
import { createContext } from "@raypx/rpc/context";
export const APIRoute = createAPIFileRoute("/api/rpc/$")({
POST: async ({ request, params }) => {
const context = await createContext(request);
const response = await appRouter.handler(request, {
context,
path: params.catchall,
});
return response;
},
});Client Usage
React Hooks
// In your React components
import { useORPC } from "@raypx/rpc/client";
function UserProfile() {
const { data, isLoading, error } = useORPC.user.profile();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}With Input
import { useORPC } from "@raypx/rpc/client";
function UserDetail({ userId }: { userId: string }) {
const { data } = useORPC.user.get({ id: userId });
return <div>{data?.name}</div>;
}Mutations
import { useORPCMuation } from "@raypx/rpc/client";
function EditProfile() {
const mutation = useORPCMuation.user.update();
const handleSubmit = async (formData: FormData) => {
await mutation.mutateAsync({
name: formData.get("name") as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" />
<button type="submit" disabled={mutation.isPending}>
Save
</button>
</form>
);
}Server-Side Calls
// In server components or server functions
import { serverClient } from "@raypx/rpc/server";
async function getUserServer(id: string) {
const user = await serverClient.user.get({ id });
return user;
}Middleware
Built-in Middleware
// packages/rpc/src/middleware.ts
import { ORPCError, os } from "@orpc/server";
const o = os.$context<Context>();
// Public procedure (no auth required)
export const publicProcedure = o;
// Protected procedure (auth required)
export const protectedProcedure = publicProcedure.use(requireAuthMiddleware);
// Admin procedure (admin role required)
export const adminProcedure = publicProcedure.use(requireAdminMiddleware);Raypx provides three procedure types:
- publicProcedure - No authentication required
- protectedProcedure - Requires authenticated user
- adminProcedure - Requires user with admin role
See Admin Features for more details on admin procedures.
Auth Middleware
// packages/rpc/middleware/auth.ts
import { middleware } from "@orpc/server";
import { getServerSession } from "@raypx/auth/server";
export const authMiddleware = middleware(async ({ context, next }) => {
const session = await getServerSession();
if (!session) {
throw new Error("Unauthorized");
}
return next({
context: {
...context,
user: session.user,
session,
},
});
});Logging Middleware
// packages/rpc/middleware/logging.ts
import { middleware } from "@orpc/server";
import { logger } from "@raypx/logger";
export const loggingMiddleware = middleware(async ({ path, input, next }) => {
const start = Date.now();
logger.info(`[ORPC] ${path}`, { input });
try {
const result = await next();
const duration = Date.now() - start;
logger.info(`[ORPC] ${path} completed`, { duration });
return result;
} catch (error) {
const duration = Date.now() - start;
logger.error(`[ORPC] ${path} failed`, { error, duration });
throw error;
}
});Rate Limiting Middleware
// packages/rpc/middleware/rateLimit.ts
import { middleware } from "@orpc/server";
import { ratelimit } from "@raypx/redis";
export const rateLimitMiddleware = middleware(async ({ context, next }) => {
const identifier = context.user?.id ?? context.ip ?? "anonymous";
const { success } = await ratelimit.limit(identifier);
if (!success) {
throw new Error("Too many requests");
}
return next();
});Context
Define Context Type
// packages/rpc/context.ts
import type { User, Session } from "@raypx/auth/types";
export interface RequestContext {
user?: User;
session?: Session;
db: typeof import("@raypx/database").db;
headers: Headers;
ip?: string;
}Create Context
// packages/rpc/context.ts
import { getServerSession } from "@raypx/auth/server";
import { db } from "@raypx/database";
export async function createContext(request: Request): Promise<RequestContext> {
const session = await getServerSession();
return {
user: session?.user,
session,
db,
headers: request.headers,
ip: request.headers.get("x-forwarded-for") ?? undefined,
};
}Error Handling
Custom Errors
// packages/rpc/errors.ts
import { ORPCError } from "@orpc/server";
export class NotFoundError extends ORPCError {
constructor(message: string) {
super({
code: "NOT_FOUND",
message,
});
}
}
export class UnauthorizedError extends ORPCError {
constructor(message = "Unauthorized") {
super({
code: "UNAUTHORIZED",
message,
});
}
}
export class ForbiddenError extends ORPCError {
constructor(message = "Forbidden") {
super({
code: "FORBIDDEN",
message,
});
}
}Using Errors
import { NotFoundError, ForbiddenError } from "../errors";
export const getPost = publicProcedure
.input(o.object({ id: o.string() }))
.handler(async ({ input, context }) => {
const post = await context.db.post.findById(input.id);
if (!post) {
throw new NotFoundError("Post not found");
}
if (post.published === false && post.authorId !== context.user?.id) {
throw new ForbiddenError("You don't have access to this post");
}
return post;
});Best Practices
Input Validation
import { o } from "@orpc/server";
// Define reusable schemas
const PaginationSchema = o.object({
page: o.number().int().min(1).default(1),
limit: o.number().int().min(1).max(100).default(20),
});
const IdSchema = o.string().uuid();
// Use in procedures
export const listPosts = publicProcedure
.input(PaginationSchema.extend({
authorId: IdSchema.optional(),
search: o.string().optional(),
}))
.handler(async ({ input }) => {
// input is fully typed and validated
});Procedure Organization
procedures/
├── user/
│ ├── get.ts
│ ├── update.ts
│ └── index.ts # Export all user procedures
├── post/
│ ├── list.ts
│ ├── create.ts
│ ├── update.ts
│ ├── delete.ts
│ └── index.ts
└── index.ts # Export all proceduresType Exports
// packages/rpc/index.ts
export type { AppRouter } from "./routers";
export type { RequestContext } from "./context";
// For client
export { createClient } from "./client";Comparison with tRPC
| Feature | ORPC | tRPC |
|---|---|---|
| Bundle Size | ~3KB | ~40KB |
| Learning Curve | Simpler | More features |
| Server-First | Yes | Yes |
| React Integration | Hooks | Hooks + SSG |
| Subscriptions | Limited | Full support |
| Plugins | Basic | Extensive |