Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

createLocalCalls

Calls the application's API and RPC inside the process, using the current request's credentials and the same guards, validation and limits as HTTP.

server Since 1.0.0 @meekojs/meeko/server Source code
ts
import { createLocalCalls } from "@meekojs/meeko/server";

createLocalCalls(options) returns two functions, api and rpc, that handle an application endpoint or procedure without leaving the process: they construct an in-memory Request with the original request's credential headers (cookie, x-meeko-csrf and authorization) and the application's origin as Origin (since 1.8; previously it forwarded the original Origin), and pass it through dispatchApiRoute or dispatchRpc. The response is the same as over HTTP: Origin, CSRF, rateLimit, auth, authorize, schemas, locals and error normalization.

In a framework application this is unnecessary: the api.* and rpc.* clients already dispatch locally during SSR. This is for your own host, task or test with a RequestContext that wants to reuse an endpoint without a network call.

Usage#

ts
import { pathToFileURL } from "node:url";
import { createLocalCalls } from "@meekojs/meeko/server";

const calls = createLocalCalls({ manifest, context, sessions, load: (file) => import(pathToFileURL(file).href) });
const user = await (await calls.api("users.byId", "GET", { params: { id: "123" } })).json();
const result = await (await calls.rpc("users.get", { id: "123" })).json();

Type#

ts
function createLocalCalls<S>(options: LocalCallOptions<S>): {
  api(clientKey: string, method: ApiMethod, input?: LocalApiInput): Promise<Response>;
  rpc(id: string, input: unknown): Promise<Response>;
}

LocalCallOptions

ts
interface LocalCallOptions<S> {
	manifest: ServerManifest
	context: RequestContext<S>
	sessions: SessionManager
	load: ServerModuleLoader
	onError?: ServerErrorObserver
}

LocalApiInput

ts
interface LocalApiInput {
  params?: Readonly<Record<string, string>>;
  query?: Readonly<
    Record<string, string | number | boolean | readonly string[] | null | undefined>
  >;
  body?: unknown;
}

Parameters#

createLocalCalls(options) takes a LocalCallOptions<S> object:

Option Type Default Description
manifest ServerManifest required Endpoints from server/api and server/rpc.
context RequestContext<S> required The original request's context: its credentials, session, services, signal, IP and requestId.
sessions SessionManager required The manager that checks Origin and CSRF.
load (file: string) => Promise<unknown> required Loads each manifest file's module.
onError (error, requestId) => void none Observer for 5xx failures.

Return value#

{ api, rpc }:

Function Description
api(clientKey, method, input?) Handles the endpoint with that client key ("users.byId", the one from api.users.byId) and method. input is { params?, query?, body? }; body is serialized to JSON and is not allowed in GET.
rpc(id, input) Handles procedure id ("users.get") with input as its body.

Both return Promise<Response> and never reject: a missing endpoint is 404 NOT_FOUND, params that do not form the route are 400 INVALID_PARAMS, and a handler failure is the usual error response, with the original request's requestId.

Examples#

A module handler that reuses an endpoint#

ts
// A custom handler that already has the request context.
const calls = createLocalCalls({ manifest: app.manifest.server, context, sessions, load });
const response = await calls.api("orders", "POST", { body: { productId: "p_1", quantity: 2 } });
if (response.status === 429) return new Response("Demasiados pedidos", { status: 429, headers: { "retry-after": response.headers.get("retry-after") ?? "60" } });
return Response.json(await response.json(), { status: response.status });

Query string with lists#

ts
await calls.api("products", "GET", { query: { tag: ["oferta", "nuevo"], page: 2, draft: undefined } });
// GET /api/products?tag=oferta&tag=nuevo&page=2 (null and undefined are omitted)

A test of a protected endpoint#

ts
const context = createRequestContext(new Request("http://localhost/", { headers: { cookie, origin: "http://localhost", "x-meeko-csrf": csrf } }), services, await sessions.load(request));
const response = await createLocalCalls({ manifest, context, sessions, load }).api("profile", "PUT", { body: { name: "Ada" } });
assert.equal(response.status, 200);

Common mistakes#

Expecting rejection#

Incorrectts
try {
  const user = await (await calls.api("users.byId", "GET", { params: { id } })).json();
} catch {
  // never reaches here with a 404
}

api and rpc always resolve with a Response. A 404 {"code":"NOT_FOUND"} arrives as a response, and .json() returns it as though it were the user.

Correctts
const response = await calls.api("users.byId", "GET", { params: { id } });
if (!response.ok) throw new Error(`users.byId ${response.status}`);
const user = await response.json();

A body in GET#

Incorrectts
await calls.api("search", "GET", { body: { q: "zapatillas" } });

Returns 400 INVALID_BODY. Use query.

Correctts
await calls.api("search", "GET", { query: { q: "zapatillas" } });

Without load#

Incorrectts
createLocalCalls({ manifest, context, sessions });

TypeError: Local calls require a module loader, on creation.

Correctts
createLocalCalls({ manifest, context, sessions, load: (file) => import(pathToFileURL(file).href) });

Notes#

  • Same guards. A local call is not a back door: it passes through CSRF with the original request's token, and through auth, authorize and schemas. Its Origin is the application's, just like SSR clients: it is a call from the server itself, so a local mutation works from a page requested with GET, which carries no Origin.
  • rateLimit. Uses the HTTP dispatcher's route counter and the IP from context, so a local call does not bypass the limit.
  • Cancellation. The internal Request carries context.signal: if the client leaves or the session is revoked, the call aborts.
  • Cookies. The response carries its Set-Cookie if the endpoint changes the session; your own host must forward it to reach the browser.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/server/createlocalcalls.md