Stone.jsDocs
Paradigm

Contexts

Backend

The backend is the context most frameworks are built around, so it is the easiest place to feel what Stone.js does differently: the server is not the foundation of your app, it is one context your domain can collapse into.

#The same domain

Here is the Tasks handler again, unchanged. It names no server, no request object, no port. It reads intentions from an event and returns values.

import { Service } from '@stone-js/core'

@Service({ alias: 'tasks' })
export class TaskService {
  private readonly items = new Map<string, Task>()

  list (done?: boolean): Task[] {
    const all = [...this.items.values()]
    return done === undefined ? all : all.filter((t) => t.done === done)
  }

  find (id: string): Task | undefined { return this.items.get(id) }

  add (title: string): Task {
    const task: Task = { id: crypto.randomUUID(), title, done: false }
    this.items.set(task.id, task)
    return task
  }
}
import { RuntimeError } from '@stone-js/core'
import { IncomingHttpEvent } from '@stone-js/http-core'
import { Get, Post, EventHandler } from '@stone-js/router'

@EventHandler('/tasks')
export class TaskController {
  private readonly tasks: TaskService
  constructor ({ tasks }: { tasks: TaskService }) { this.tasks = tasks }

  @Get('/')
  list (event: IncomingHttpEvent): Task[] {
    return this.tasks.list(event.get<boolean>('done'))
  }

  @Get('/:id')
  show (event: IncomingHttpEvent): Task {
    const task = this.tasks.find(event.get<string>('id'))
    if (task === undefined) throw new RuntimeError('Task not found')
    return task
  }

  @Post('/')
  create (event: IncomingHttpEvent): Task {
    return this.tasks.add(event.get<string>('title'))
  }
}

#Collapse it onto Node

Only the manifest changes per context. Add the Node HTTP adapter and you have a real HTTP service. Add the CLI adapter alongside it and the very same domain answers to commands too. Neither adapter touches Tasks.

app/Application.tsdeclarativeimperative
import { StoneApp } from '@stone-js/core'
import { Routing } from '@stone-js/router'
import { NodeHttp } from '@stone-js/node-http-adapter'
import { NodeConsole } from '@stone-js/node-cli-adapter'

@NodeHttp()      // serve the domain over HTTP on Node
@NodeConsole()   // ...and expose the same domain as CLI commands
@Routing()
@StoneApp()
export class Application {}
import { defineStoneApp } from '@stone-js/core'
import { routerBlueprint } from '@stone-js/router'
import { nodeHttpAdapterBlueprint } from '@stone-js/node-http-adapter'
import { nodeConsoleAdapterBlueprint } from '@stone-js/node-cli-adapter'

export const App = defineStoneApp(
  { name: 'tasks' },
  [routerBlueprint, nodeHttpAdapterBlueprint, nodeConsoleAdapterBlueprint]
)
terminalbash
npm run dev
# HTTP:
curl localhost:8080/tasks
# CLI (same domain, different cause):
node app tasks:list
The server did not define the app. The app accepted the server.

#What the backend context gives you

  • node-http-adapter: a production HTTP server, streaming, cookies, static files.
  • node-cli-adapter: the same handlers, invoked as commands, for jobs and tooling.
  • http-core: the runtime-agnostic HTTP layer both share, so semantics stay identical.

Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)