Start here
Your first domain
The domain is the part of your application that would still be true on any platform: its entities, its rules, its use cases. You write it first, and you write it without ever naming a server, a request object or a runtime.
#A service and a handler
Two pieces. A service holds the logic and the state; a handler exposes intentions and delegates to the service. The handler reads values off an event and returns plain data, nothing platform-shaped.
import { Service } from '@stone-js/core'
interface Task { id: string, title: string, done: boolean }
@Service({ alias: 'tasks' })
export class TaskService {
private readonly items = new Map<string, Task>()
list (): Task[] { return [...this.items.values()] }
add (title: string): Task {
const task: Task = { id: crypto.randomUUID(), title, done: false }
this.items.set(task.id, task)
return task
}
}import { defineService } from '@stone-js/core'
const TaskService = () => {
const items = new Map()
return {
list: () => [...items.values()],
add: (title) => {
const task = { id: crypto.randomUUID(), title, done: false }
items.set(task.id, task)
return task
}
}
}
export const services = [defineService(TaskService, { alias: 'tasks' }, true)]#What is deliberately missing
There is no port, no listen(), no request or response type, no mention of Node or the browser. The handler takes an intention (event.get('title')) and returns a value. Where that intention comes from, and where the value goes, is the context's job, added next.
Write the what. The where comes later, and never leaks back in.
#Injection, not lookup
The controller does not fetch its service; it receives it. @Service({ alias: 'tasks' }) registers the service under a name, and the container hands it to the controller's constructor. Everything flows toward the domain, which is what keeps the domain unaware of everything around it.
constructor ({ tasks }: { tasks: TaskService }) {
this.tasks = tasks // resolved from the container by its alias
}