Microservices: Service-as-URL Forwarding
Lesan lets you split a backend into multiple applications and route requests between them. A service in Lesan is either:
- a local
Actsobject (in-process), or - a URL string pointing at another Lesan server's
POST /lesanendpoint (remote proxy).
Both are registered with setService, and the client never needs to know which is which โ it just sends service: "ecommerce".
Registering a Serviceโ
import { lesan } from "@hemedani/lesan";
export const coreApp = lesan();
// Remote: forward "ecommerce" requests to another server's /lesan endpoint.
coreApp.acts.setService("ecommerce", "http://localhost:8574/lesan");
// Or in-process: point at another app's local acts.
// coreApp.acts.setService("ecommerce", ecommerceActs);
The signature is setService(serviceName, service: Acts | string):
coreApp.acts.setService("ecommerce", "http://localhost:8574/lesan"); // URL โ proxied
coreApp.acts.setService("ecommerce", ecommerceMainActs); // Acts โ in-process
How Forwarding Worksโ
When a request arrives with service: "ecommerce" and the service value is a string, serveLesan:
- Reads the service value as the remote URL.
- Rewrites the incoming body's
serviceto"main"(the remote app handles it with its ownmainacts). POSTs the whole body tohttp://localhost:8574/lesanwith the original headers.- Returns the remote response to the client.
// core app forwards: { service: "ecommerce", model: "state", act: "getStates", details: {...} }
// becomes: { service: "main", model: "state", act: "getStates", details: {...} }
So the ecommerce server is a normal Lesan server running on port 8574:
// ecommerce app
coreApp.runServer({ port: 8574, playground: false, typeGeneration: true });
Registering the Local (Main) Actsโ
If you only use remote services, main is still required for the core app's own acts. Register them with setAct as usual โ acts defaults to main when the client omits service.
coreApp.acts.setAct({
schema: "user",
actName: "getUser",
validator: getUserValidator(),
fn: getUser,
});
Introspecting Servicesโ
coreApp.acts.getServiceKeys(); // ["main", "ecommerce", ...]
coreApp.acts.getService("ecommerce"); // the Acts object or URL string
coreApp.acts.getActsWithServices(); // all acts grouped by service
getActsKeys(serviceName) throws if the service value is a string (a URL has no local acts to enumerate) โ only inspect acts for in-process services.
Client Experienceโ
The client sends one body shape regardless of where the act lives:
{
"service": "ecommerce",
"model": "state",
"act": "getStates",
"details": {
"set": {
"pagination": { "lastObjectId": "507f1f77bcf86cd799439011", "limit": 20 }
},
"get": {
"name": 1,
"country": 1,
"cities": 1
}
}
}
The get projection is forwarded untouched, so the ecommerce server's selectStruct validator can shape the response exactly like a local call.
Validators Across Servicesโ
On the ecommerce side, selectStruct supports deep per-relation projections โ a pattern that's handy when services pass documents back and forth:
const getStateValidator = () =>
object({
set: object({ stateId: objectIdValidation }),
get: coreApp.schemas.selectStruct("state", {
country: {
states: { country: 1 },
},
}),
});
Real-World Layoutโ
A typical setup (mirroring the examples/microservice folder):
core app (:8585) โ users, auth, main domain acts
โโ setService("ecommerce", "http://localhost:8574/lesan")
ecommerce app (:8574) โ states, cities, orders (its own main acts)
The core server proxies any service: "ecommerce" request to port 8574; the ecommerce server handles it with service: "main" internally and replies. No HTTP client code, no manual fetch calls, no duplicated validation logic.