alepha@docs:~/docs/guides/server$
cat 6-http-links.md
3 min read
Last commit:

#HTTP Links

Alepha's link system provides type-safe cross-service communication through $client and $remote. The same API works for local calls (in-process), remote calls (HTTP), and browser-to-server calls.

#$client -- Type-Safe Action Proxy

$client<T>() creates a proxy object that mirrors the actions of a controller class. Property access on the proxy returns virtual actions that can be called like functions.

typescript
 1import { $client } from "alepha/server/links"; 2import { $action } from "alepha/server"; 3import { t } from "alepha"; 4  5class ProductController { 6  getProduct = $action({ 7    path: "/products/:id", 8    schema: { 9      params: t.object({ id: t.uuid() }),10      response: t.object({ id: t.uuid(), name: t.text(), price: t.number() }),11    },12    handler: async ({ params }) => {13      return await this.repo.findById(params.id);14    },15  });16}17 18class OrderService {19  products = $client<ProductController>();20 21  createOrder = $action({22    method: "POST",23    path: "/orders",24    schema: {25      body: t.object({ productId: t.uuid(), quantity: t.integer() }),26    },27    handler: async ({ body }) => {28      // Type-safe call: params type is inferred from ProductController.getProduct29      const product = await this.products.getProduct({30        params: { id: body.productId },31      });32      return { product: product.name, total: product.price * body.quantity };33    },34  });35}

#Local vs Remote Resolution

When the target action exists in the same process, $client calls the handler directly with no HTTP overhead. When the action is on a remote service, it makes an HTTP request. This is transparent to the caller.

#Virtual Actions

Each property on a $client proxy returns a VirtualAction with these methods:

Method Description
action(config) Default call. Local-first -- calls the handler directly if available, otherwise HTTP.
action.run(config) Same as calling the action directly. Local-first.
action.fetch(config) Always makes an HTTP request, even if the action is local.
action.can() Returns true if the current user has permission to call this action.
action.schema() Returns the body and response schemas of the action.
typescript
 1// Direct call (local-first) 2const product = await this.products.getProduct({ params: { id } }); 3  4// Force HTTP 5const response = await this.products.getProduct.fetch({ params: { id } }); 6  7// Permission check 8if (this.products.getProduct.can()) { 9  // user has access10}11 12// Schema introspection13const schemas = this.products.getProduct.schema();14// schemas.body, schemas.response

#$remote -- Remote Service Access

$remote defines a connection to an external service. Use it when services run as separate deployments.

typescript
 1import { $remote } from "alepha/server/links"; 2import { $env, t } from "alepha"; 3  4class Gateway { 5  env = $env(t.object({ 6    PAYMENTS_URL: t.text({ default: "http://localhost:4000" }), 7  })); 8  9  payments = $remote({10    url: this.env.PAYMENTS_URL,11  });12}

Auto-discovery of remote services is not currently supported. The $remote URL must be configured manually. Future versions may support service discovery via Redis.

#Service Account Authentication

For authenticated service-to-service communication, attach a service account:

typescript
 1import { $remote } from "alepha/server/links"; 2import { $serviceAccount } from "alepha/security"; 3  4class Gateway { 5  sa = $serviceAccount({ secret: "shared-secret" }); 6  7  payments = $remote({ 8    url: "https://payments.internal", 9    serviceAccount: this.sa,10  });11}

#Proxying

Set proxy: true to expose the remote service's endpoints through the current server:

typescript
1payments = $remote({2  url: "https://payments.internal",3  proxy: true,4});5// Remote endpoints are now accessible via this server

This is useful when you have a backend-for-frontend (BFF) pattern and want to aggregate multiple services under a single API.

#Browser Usage

In React, use useClient<T>() to call server actions from the browser:

typescript
 1import { useClient } from "alepha/react"; 2  3function ProductPage() { 4  const api = useClient<ProductController>(); 5  6  const loadProduct = async (id: string) => { 7    const product = await api.getProduct({ params: { id } }); 8    // product is fully typed 9  };10}

In the browser, all calls go through HTTP. During SSR, local actions are called directly.

#How Links Work

The LinkProvider maintains a registry of all available actions (local and remote). When a $client proxy is accessed:

  1. It looks up the action by name in the link registry.
  2. If the action has a local handler (same process), it calls the handler directly.
  3. If the action is remote (has a host), it makes an HTTP request via HttpClient.
  4. Authorization headers from the current request context are forwarded automatically.

The proxy is built using JavaScript Proxy, so property access is intercepted at runtime and mapped to link lookups.