Skip to content
This repository was archived by the owner on Mar 20, 2026. It is now read-only.

Repository files navigation

ServiceX

Platform-agnostic DDD service framework. Define services once, run anywhere.

Packages

PackageDescriptionFor
servicexjsFluent API + all domain primitivesUsers — the only import you need
@servicexjs/nodeNode.js runtime adapterUsers — dev & testing
@servicexjs/coreInternal building blocksRuntime adapters — not for direct use

Quick Start

bun add servicexjs @servicexjs/node

Define a Service

import{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}));

Call the Service

# 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" } }

Architecture

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)

Core Concepts

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.

API Reference

servicexjs

createService(name: string): ServiceBuilder

Returns a fluent builder:

MethodDescription
.rpc(methods)Declare RPC method handlers
.register(fn)Declare dependency registration
.publicMethods(list)Declare unauthenticated methods
.run(runtime)Bind to a platform runtime

servicexjs — Domain & Utilities

Everything below is available from import { ... } from "servicexjs".

Domain:

ExportDescription
Entity<T>Base class for domain entities
ValueObject<T>Base class for value objects
Id.generate(prefix)Generate prefixed unique IDs
DomainErrorBase domain error
ValidationError400 — invalid input
AuthenticationError401 — not authenticated
ForbiddenError403 — not authorized
NotFoundError404 — entity not found
ConflictError409 — duplicate/conflict

Repository:

ExportDescription
Repository<T>Interface — findById, save, delete
DrizzleRepository<T, Table>Base class with upsert save, findById, delete

Decorators:

ExportDescription
@injectable()Mark a class for DI resolution
@inject(token)Inject a dependency by token
@singleton()Mark as singleton

Types:

ExportDescription
Runtime<T>Interface for platform runtime adapters
RpcContextContext passed to RPC handlers (auth, resolve, env)
AuthContextAuthenticated user info (userId, tenantId, email)
PlatformEvent<Type, Payload>Event contract for cross-service messaging
createEvent(type, payload)Helper to create typed events

@servicexjs/node

node(config?: NodeConfig): Runtime<{app: Hono;port: number}>
OptionDefaultDescription
port3000Port 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

RPC Method Handler

typeRpcMethodHandler=(params: any,ctx: RpcContext)=>Promise<any>;interfaceRpcContext{auth: AuthContext;// authenticated userresolve<T>(token: string): T;// resolve dependencyenv: Record<string,unknown>;// platform environment}

Custom Runtime Adapter

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},};}

License

MIT

About

Platform-agnostic DDD service framework — core abstractions + pluggable platform adapters

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages