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.
Se autoimporta en el código de servidor: server/ y <script server>.
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#
// 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() });// 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#
function defineRpc<
I extends Schema,
O extends Schema,
S = unknown,
L = undefined,
>(definition: RpcDefinition<I, O, S, L>): RpcDefinition<I, O, S, L>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:
// 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#
// 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),
});<!-- 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#
// 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#
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.
export default defineRpc({ input: newOrder, output: orderSchema, handler: ({ input }) => ({ id: "1", ...input }) });status en un RPC#
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.
export default defineRpc({ input: newOrder, output: orderSchema, handler: ({ input }) => ({ id: "1", ...input }) });Un procedimiento y una carpeta con el mismo nombre#
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.
server/rpc/orders/list.ts ← rpc.orders.list(…)
server/rpc/orders/create.ts ← rpc.orders.create(…)Notas#
- Seguridad. Cada llamada exige el
Originpermitido (desde1.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 confetchde Node ocurlsinOriginresponde403 INVALID_ORIGIN. El middleware de ruta no protege el RPC: usaauth,authorizeylocals. - Orden.
Origin(desde1.8, en toda llamada salvo conorigin: "none"),Originy CSRF de la sesión,rateLimit,auth,authorize,input,locals,handleryoutput. - 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; sinawait, la lista sale vacía en el HTML ymeeko checkavisa conMEEK117del salto de diseño. - Errores. Escribe los mensajes en el schema de
input: viajan enissuesyuseFormlos reparte por campo. Un error que no esApiErrornunca expone su mensaje. - Nombres. El nombre del procedimiento sale de la ruta del archivo (
orders/create.tsesorders.create); los schemas deben estar enshared/, o ser desde 1.8 una composición simple de ellos con funciones de un paquete, comov.array(itemSchema), y tener tipos concretos (nianyniunknown). Véase Schemas públicos.