Platform-agnostic DDD service framework. Define services once, run anywhere.
| Package | Description | For |
|---|---|---|
servicexjs | Fluent API + all domain primitives | Users — the only import you need |
@servicexjs/node | Node.js runtime adapter | Users — dev & testing |
@servicexjs/core | Internal building blocks | Runtime adapters — not for direct use |
bun add servicexjs @servicexjs/nodeimport{createService,Entity,Id,injectable,inject,DrizzleRepository}from"servicexjs";import{node}from"@servicexjs/node";// 1. Domain — define your entityclassTenantextendsEntity<string>{readonlyname: string;privateconstructor(id: string,name: string){super(id);this.name=name;}staticcreate(name: string): Tenant{returnnewTenant(Id.generate("tnt"),name);}staticreconstitute(id: string,name: string): Tenant{returnnewTenant(id,name);}}// 2. Repository — extend DrizzleRepository
@injectable()classTenantRepoextendsDrizzleRepository<Tenant,typeoftenants>{constructor(@inject("DB")db: any){super(db,tenants);}protectedtoEntity(row: any): Tenant{returnTenant.reconstitute(row.id,row.name);}protectedtoRow(entity: Tenant){return{id: entity.id,name: entity.name};}}// 3. Service — business logic
@injectable()classTenantService{constructor(@inject("TenantRepo")privaterepo: TenantRepo){}asynccreate(name: string){consttenant=Tenant.create(name);awaitthis.repo.save(tenant);returntenant;}asyncget(id: string){returnthis.repo.findById(id);}}// 4. Wire it up — createService().rpc().register().run()exportdefaultcreateService("tenant").register((ctx,env)=>{ctx.value("DB",env.DB);ctx.bind("TenantRepo",TenantRepo);ctx.bind("TenantService",TenantService);}).rpc({"tenant.create": async(params,ctx)=>{constsvc=ctx.resolve<TenantService>("TenantService");returnsvc.create(params.name);},"tenant.get": async(params,ctx)=>{constsvc=ctx.resolve<TenantService>("TenantService");returnsvc.get(params.id);},}).run(node({port: 3000}));# RPC request format
curl -X POST http://localhost:3000/api/rpc \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"method": "tenant.create", "params": {"name": "My Team"}}'# Response# { "result": { "id": "tnt_m1abc_x7f3k2p1", "name": "My Team" } }createService("name") ← servicexjs (fluent API)
.register(...) ← DI registration (platform-agnostic)
.rpc({ ... }) ← RPC method declarations
.run(node({ port: 3000 })) ← bind to runtime, start service
│
├── @servicexjs/node (open source, dev & testing)
└── your-cloud-runtime (your own adapter for production)
ServiceContainer — defines what a service IS (dependencies + RPC methods), without coupling to any platform.
Runtime — decides HOW to run it (Node.js, Cloudflare Workers, AWS Lambda, etc.). Runtimes implement the Runtime<T> interface from @servicexjs/core.
Everything before .run() is platform-agnostic. Switch runtimes by changing one line.
createService(name: string): ServiceBuilderReturns a fluent builder:
| Method | Description |
|---|---|
.rpc(methods) | Declare RPC method handlers |
.register(fn) | Declare dependency registration |
.publicMethods(list) | Declare unauthenticated methods |
.run(runtime) | Bind to a platform runtime |
Everything below is available from import { ... } from "servicexjs".
Domain:
| Export | Description |
|---|---|
Entity<T> | Base class for domain entities |
ValueObject<T> | Base class for value objects |
Id.generate(prefix) | Generate prefixed unique IDs |
DomainError | Base domain error |
ValidationError | 400 — invalid input |
AuthenticationError | 401 — not authenticated |
ForbiddenError | 403 — not authorized |
NotFoundError | 404 — entity not found |
ConflictError | 409 — duplicate/conflict |
Repository:
| Export | Description |
|---|---|
Repository<T> | Interface — findById, save, delete |
DrizzleRepository<T, Table> | Base class with upsert save, findById, delete |
Decorators:
| Export | Description |
|---|---|
@injectable() | Mark a class for DI resolution |
@inject(token) | Inject a dependency by token |
@singleton() | Mark as singleton |
Types:
| Export | Description |
|---|---|
Runtime<T> | Interface for platform runtime adapters |
RpcContext | Context passed to RPC handlers (auth, resolve, env) |
AuthContext | Authenticated user info (userId, tenantId, email) |
PlatformEvent<Type, Payload> | Event contract for cross-service messaging |
createEvent(type, payload) | Helper to create typed events |
node(config?: NodeConfig): Runtime<{app: Hono;port: number}>| Option | Default | Description |
|---|---|---|
port | 3000 | Port to listen on |
auth.secret | "dev-secret" | JWT secret for dev/test |
auth.cookieName | "session" | Session cookie name |
env | {} | Environment variables to inject |
basePath | "/api" | API route prefix |
typeRpcMethodHandler=(params: any,ctx: RpcContext)=>Promise<any>;interfaceRpcContext{auth: AuthContext;// authenticated userresolve<T>(token: string): T;// resolve dependencyenv: Record<string,unknown>;// platform environment}Implement Runtime<T> to create your own platform adapter:
importtype{Runtime,ServiceDefinition}from"@servicexjs/core";functionmyRuntime(config: MyConfig): Runtime<MyOutput>{return{create(definition: ServiceDefinition): MyOutput{// 1. Create ServiceContainerImpl and initialize with env// 2. Wire up HTTP server with definition.methods// 3. Handle auth for non-public methods// 4. Map DomainErrors to HTTP status codes// 5. Return platform-specific output},};}MIT