#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.
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. |
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.
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
$remoteURL 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:
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:
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:
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:
- It looks up the action by name in the link registry.
- If the action has a local handler (same process), it calls the handler directly.
- If the action is remote (has a
host), it makes an HTTP request viaHttpClient. - 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.