Saltar al contenido
Meeko
Escribe para buscar en las guías y en la referencia de la API. Ábrela desde cualquier página con / o Ctrl K; muévete con las flechas y entra con Intro.
Empezar

Contenidos

En esta página

defineRpc

Declara un procedimiento RPC de server/rpc con sus schemas de entrada y salida, guardas y handler, que la aplicación llama con rpc.* tipado.

macro Desde 1.0.0 Sin import @meekojs/meeko/server Código fuente

Se autoimporta en el código de servidor: server/ y <script server>.

ts
import { defineRpc } from "@meekojs/meeko/server";

defineRpc declara un procedimiento RPC: el export por defecto de server/rpc/<ruta>.ts. server/rpc/orders/create.ts es el procedimiento orders.create, que se llama con POST /_meeko/rpc/orders/create y, desde la aplicación, con rpc.orders.create(input), useRpcQuery o useRpcMutation, con los tipos de sus schemas.

Frente a defineApiRoute, un RPC no modela recursos HTTP: recibe un input, devuelve un output y siempre responde 200. Es la forma más corta para acciones de la propia aplicación.

Uso#

ts
// shared/schemas/orders.ts
import * as v from "valibot";

export const newOrder = v.object({ item: v.pipe(v.string(), v.minLength(1, "Elige un producto.")), qty: v.number() });
export const orderSchema = v.object({ id: v.string(), item: v.string(), qty: v.number() });
ts
// server/rpc/orders/create.ts
import { defineRpc } from "@meekojs/meeko/server";
import { newOrder, orderSchema } from "../../../shared/schemas/orders";

export default defineRpc({
  input: newOrder,
  output: orderSchema,
  auth: "required",
  handler: async ({ input, context }) => createOrder(context.session.current!.data, input),
});

En la aplicación, await rpc.orders.create({ item: "té", qty: 2 }) devuelve { id: string; item: string; qty: number }.

Tipo#

ts
function defineRpc<
  I extends Schema,
  O extends Schema,
  S = unknown,
  L = undefined,
>(definition: RpcDefinition<I, O, S, L>): RpcDefinition<I, O, S, L>

RpcDefinition

ts
interface RpcDefinition<
  I extends Schema,
  O extends Schema,
  S = unknown,
  L = undefined,
> {
  input: I;
  output: O;
  auth?: "required" | "optional";
  origin?: "required" | "none";
  rateLimit?: RateLimitOptions<S>;
  authorize?: (context: RequestContext<S>) => boolean | Promise<boolean>;
  locals?: (context: RequestContext<S>) => L | Promise<L>;
  handler: (args: {
    input: Output<I>;
    context: RequestContext<S>;
    locals: L;
  }) => Promise<Input<O> | Output<O>> | Input<O> | Output<O>;
}

Opciones#

Opción Tipo Por defecto Descripción
input Standard Schema de shared/ o composición simple de ellos obligatorio La entrada. Una que no lo supera responde 400 INVALID_INPUT con issues.
output Standard Schema de shared/ o composición simple de ellos (v.array(itemSchema), desde 1.8) obligatorio El resultado del handler. Uno que no lo supera responde 500 sin detalle.
auth "required" | "optional" "optional" Con "required", una llamada sin sesión responde 401.
authorize (context) => boolean | Promise<boolean> ninguno Con false, 403 FORBIDDEN.
origin "required" | "none" Origin obligatorio (desde 1.8) Desde 1.8, toda llamada exige un Origin permitido, haya o no sesión; "required" lo deja explícito. "none" (inseguro) lo desactiva en las llamadas sin sesión, para un servicio o una app sin Origin; con sesión siguen Origin y CSRF.
rateLimit { limit, window, key? } ninguno Desde 1.2. Límite de llamadas por ventana; por encima, 429 con Retry-After. Sin key, cuenta por IP del cliente (context.clientAddress; tras Cloudflare Tunnel, MEEKO_TRUST_PROXY=cloudflare).
locals (context) => L | Promise<L> ninguno Desde 1.2. Un valor por llamada que el handler recibe tipado. Decláralo antes de handler.
handler ({ input, context, locals }) => output obligatorio La lógica. context es el RequestContext.

Valor devuelto#

La misma definición, con sus tipos inferidos. Las respuestas siguen las de la API: 200 con el output, 400 INVALID_INPUT con issues, el estado de un ApiError y 500 INTERNAL_ERROR para cualquier otro error. No hay status: un RPC siempre responde 200.

Ejemplos#

Un inicio de sesión#

Desde 1.8, toda llamada exige un Origin permitido aunque todavía no haya sesión; origin: "required" lo deja explícito en el inicio de sesión, y rateLimit frena los intentos:

ts
// server/rpc/auth/login.ts
import { ApiError, defineRpc } from "@meekojs/meeko/server";
import { verifyPassword } from "@meekojs/meeko/password";
import { loginInput, loginOutput } from "../../../shared/schemas/auth";

export default defineRpc({
  input: loginInput,
  output: loginOutput,
  origin: "required",
  rateLimit: { limit: 5, window: "5m" },
  handler: async ({ input, context }) => {
    const user = await findUserByEmail(input.email);
    if (!user || !(await verifyPassword(input.password, user.passwordHash))) throw new ApiError("INVALID_CREDENTIALS", 401);
    await context.session.create({ userId: user.id });
    return { ok: true };
  },
});

Datos en un componente#

ts
// server/rpc/orders/list.ts
import { defineRpc } from "@meekojs/meeko/server";
import { orderFilter, orderList } from "../../../shared/schemas/orders";

export default defineRpc({
  input: orderFilter,
  output: orderList,
  auth: "required",
  handler: ({ input, context }) => listOrders(context.session.current!.data, input.status),
});
meeko
<!-- app/pages/orders.meeko -->
<script lang="ts">
const status = signal<"open" | "closed">("open");
const orders = await useRpcQuery("orders.list", () => ({ status: status.value }));
const create = useRpcMutation("orders.create");
</script>

<template>
  <ul><li each:of="orders.data.value" each:as="order" each:key="order.id">{order.item} × {order.qty}</li></ul>
  <button type="button" bind:disabled="create.pending.value" on:click="() => create.mutate({ item: 'té', qty: 1 })">Pedir té</button>
</template>

Un usuario cargado con locals#

ts
// server/rpc/profile/update.ts
import { ApiError, defineRpc } from "@meekojs/meeko/server";
import { profileInput, profileOutput } from "../../../shared/schemas/profile";

export default defineRpc({
  input: profileInput,
  output: profileOutput,
  auth: "required",
  locals: async ({ session }) => {
    const user = await findUser((session.current!.data as { userId: string }).userId);
    if (!user) throw new ApiError("UNAUTHENTICATED", 401);
    return { user };
  },
  handler: ({ input, locals }) => updateProfile(locals.user, input),
});

Errores comunes#

Sin input u output#

Incorrectots
export default defineRpc({ input: newOrder, handler: ({ input }) => ({ id: "1", ...input }) });

meeko check se detiene con server/rpc/orders/create.ts: el RPC requiere schemas públicos input y output.

Correctots
export default defineRpc({ input: newOrder, output: orderSchema, handler: ({ input }) => ({ id: "1", ...input }) });

status en un RPC#

Incorrectots
export default defineRpc({ input: newOrder, output: orderSchema, status: 201, handler: ({ input }) => ({ id: "1", ...input }) });

meeko check da TS2353: Object literal may only specify known properties, and 'status' does not exist in type 'RpcDefinition<…>'. Para elegir el estado, usa defineApiRoute.

Correctots
export default defineRpc({ input: newOrder, output: orderSchema, handler: ({ input }) => ({ id: "1", ...input }) });

Un procedimiento y una carpeta con el mismo nombre#

Incorrectotexto
server/rpc/orders.ts          ← rpc.orders(…)
server/rpc/orders/create.ts   ← rpc.orders.create(…)

Colisión de procedimientos RPC: server\rpc\orders.ts (orders) y server\rpc\orders\create.ts (orders.create) no pueden ser a la vez rutas invocables del cliente.

Correctotexto
server/rpc/orders/list.ts     ← rpc.orders.list(…)
server/rpc/orders/create.ts   ← rpc.orders.create(…)

Notas#

  • Seguridad. Cada llamada exige el Origin permitido (desde 1.8, también sin sesión; origin: "none" lo desactiva sólo sin sesión) y, con sesión, el token CSRF; el cliente generado envía ambos solo. Una llamada con fetch de Node o curl sin Origin responde 403 INVALID_ORIGIN. El middleware de ruta no protege el RPC: usa auth, authorize y locals.
  • Orden. Origin (desde 1.8, en toda llamada salvo con origin: "none"), Origin y CSRF de la sesión, rateLimit, auth, authorize, input, locals, handler y output.
  • SSR. En el servidor, rpc.* despacha localmente con la sesión de la petición original, sin salir a la red. await useRpcQuery(…) carga los datos en el SSR; sin await, la lista sale vacía en el HTML y meeko check avisa con MEEK117 del salto de diseño.
  • Errores. Escribe los mensajes en el schema de input: viajan en issues y useForm los reparte por campo. Un error que no es ApiError nunca expone su mensaje.
  • Nombres. El nombre del procedimiento sale de la ruta del archivo (orders/create.ts es orders.create); los schemas deben estar en shared/, o ser desde 1.8 una composición simple de ellos con funciones de un paquete, como v.array(itemSchema), y tener tipos concretos (ni any ni unknown). Véase Schemas públicos.

Relacionado#

¿Te sirvió esta página?

Añadir un comentario

Se envía al elegir «Sí» o «No». Solo se guardan la página, tu respuesta y el comentario: ni cookies ni datos tuyos.

0 de 2000 caracteres

Editar en GitHub: docs/reference/macros/definerpc.md