RPC

Bueno's RPC module lets you define server-side procedures and call them from the client with full type safety — no code generation, no schema files.

Defining procedures

// src/rpc/router.ts
import { createRpcRouter } from '@buenojs/bueno/rpc';
import { z } from 'zod';

export const rpcRouter = createRpcRouter({
  users: {
    list: async () => {
      return await db.query('SELECT id, name, email FROM users');
    },

    getById: async ({ id }: { id: number }) => {
      const [user] = await db.query(
        'SELECT * FROM users WHERE id = $1',
        [id]
      );
      return user ?? null;
    },

    create: async (input: { name: string; email: string }) => {
      const [user] = await db.query(
        'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
        [input.name, input.email]
      );
      return user;
    },
  },
});

export type AppRouter = typeof rpcRouter;

Mounting the server

import { rpcHandler } from '@buenojs/bueno/rpc';
import { rpcRouter } from './rpc/router';

app.all('/rpc/*', rpcHandler(rpcRouter));

Client usage

// client.ts
import { createRpcClient } from '@buenojs/bueno/rpc';
import type { AppRouter } from '../server/rpc/router';

const rpc = createRpcClient<AppRouter>({
  baseUrl: 'http://localhost:3000/rpc',
});

// Fully typed — your IDE knows the return type
const users = await rpc.users.list();
const user = await rpc.users.getById({ id: 1 });
const newUser = await rpc.users.create({ name: 'Alice', email: 'alice@example.com' });

Context

Procedures receive the request context as a second argument:
export const rpcRouter = createRpcRouter({
  posts: {
    create: async (input: CreatePostInput, ctx: Context) => {
      const user = ctx.get('jwtPayload');
      if (!user) throw new RpcError('UNAUTHORIZED', 'Login required');
      // ...
    },
  },
});

Error handling

import { RpcError } from '@buenojs/bueno/rpc';

// Server-side
throw new RpcError('NOT_FOUND', 'User not found');
throw new RpcError('VALIDATION_ERROR', 'Invalid input', { field: 'email' });

// Client-side
try {
  const user = await rpc.users.getById({ id: 999 });
} catch (err) {
  if (err instanceof RpcError && err.code === 'NOT_FOUND') {
    console.log('User does not exist');
  }
}

Middleware

Apply middleware to RPC procedures:
export const rpcRouter = createRpcRouter(
  {
    admin: {
      deleteAll: async () => { /* ... */ },
    },
  },
  { middleware: [requireAdmin] }
);

vs REST vs GraphQL

RPC REST GraphQL
Type safety ✓ End-to-end With codegen With codegen
Flexibility Procedures only Resources Any query shape
Learning curve Minimal Low Higher
Best for Internal, monorepo Public APIs Complex data graphs

Released under the MIT License. Built with love by the Bueno Community.