このガイドでは、小さなHTTP Applicationを作り、portableなKernel Hostからテストし、Node.jsへ接続します。
プロジェクトを作成する
npm create loutre@latest my-app
cd my-app
TargetにはNode.js、Bun、Cloudflare Workers、AWS Lambdaを選べます。
HTTP executionを定義する
HTTP semanticsはHTTP Extensionに置き、生成されたExecution DefinitionをModuleへ登録します。
src/greetings/contract.ts:
import { http } from '@loutrejs/loutre/http'
import { z } from 'zod'
export const GreetingContract = http.contract({
greet: {
method: 'GET',
path: '/greetings/{name}',
request: {
params: {
name: z.string().min(1),
},
},
responses: {
ok: {
status: 200,
body: z.object({ message: z.string() }),
},
},
},
})
src/greetings/service.ts:
export class GreetingService {
greet(name: string) {
return { message: `こんにちは、${name}!` }
}
}
src/greetings/controller.ts:
import { inject } from '@loutrejs/loutre'
import { http } from '@loutrejs/loutre/http'
import { GreetingContract } from './contract.js'
import { GreetingService } from './service.js'
export const GreetingHttp = http.implementation({
name: 'GreetingHttp',
contract: GreetingContract,
factory: (greetings = inject(GreetingService)) => ({
async greet(ctx) {
return ctx.response.ok({
body: greetings.greet(ctx.input.params.name),
})
},
}),
})
src/app.ts:
import { defineApplication, defineModule } from '@loutrejs/loutre'
import { GreetingHttp } from './greetings/controller.js'
import { GreetingService } from './greetings/service.js'
const AppModule = defineModule({
providers: [GreetingService],
executions: [GreetingHttp],
})
export default defineApplication({
modules: [AppModule],
})
defineApplication()がcanonical Application Modelを構築します。HTTP routeの詳細はHTTP Extensionが所有し、Coreはprotocol固有の分岐を持ちません。
静的Moduleと動的Module
引数のないModuleはdefineModule({ ... })で定義し、返されたinstanceをそのままmodulesやimportsへ渡します。設定が必要なModuleはfactoryを渡して、利用時に引数を指定します。
const UsersModule = defineModule({ providers: [UsersService] })
const DatabaseModule = defineModule((options: { url: string }) => ({
providers: [provide(DatabaseUrl).useValue(options.url)],
exports: [DatabaseUrl],
}))
const AppModule = defineModule({
imports: [UsersModule, DatabaseModule({ url: 'postgres://localhost/app' })],
})
同じinstanceの参照はGraph内で共有されます。動的factoryを明示的に呼ぶたびに別instanceが作られるので、共有したい場合は結果を変数に保存してください。旧形式のdefineModule(() => ({ ... }))()も利用できます。引数なしの旧templateを直接modulesやimportsへ渡す場合は、Model構築ごとに同じtemplateを一つのinstanceとして正規化します。異なるApplicationの構築間ではfactoryの結果を共有しません。
portable Kernel Hostからテストする
import { bootstrapApplication } from '@loutrejs/loutre'
import { bindHttpServer } from '@loutrejs/loutre/http'
import { expect, it } from 'vitest'
import application from './app.js'
it('挨拶を返す', async () => {
const app = await bootstrapApplication({
application,
capabilities: [bindHttpServer({ runtime: 'test' })],
})
try {
const response = await app.http.fetch(
new Request('http://localhost/greetings/Loutre'),
)
expect(response.status).toBe(200)
await expect(response.json()).resolves.toEqual({
message: 'こんにちは、Loutre!',
})
} finally {
await app.close('test-complete')
}
})
bootstrapApplication()はKernelを初期化し、Application Modelに含まれるExtensionがcontributeしたHost APIだけを公開します。
Node.jsへ接続する
src/main.ts:
import { nodeRuntime } from '@loutrejs/node'
import application from './app.js'
const app = await nodeRuntime.create({ application })
await app.serve({ port: 3000 })
npm run dev
curl http://localhost:3000/greetings/Loutre
{ "message": "こんにちは、Loutre!" }
Application Definitionはportableなままです。platform固有のlistenerやcallback boundaryはRuntime adapterが所有します。
Environmentを追加する
import { defineEnv, defineModule } from '@loutrejs/loutre'
import { z } from 'zod'
export class AppEnv extends defineEnv(
z.object({ PORT: z.coerce.number().int().positive().default(3000) }),
) {}
const AppModule = defineModule({
environment: [AppEnv],
executions: [GreetingHttp],
})
Application初期化後はvalidation済みの値を利用できます。
const app = await nodeRuntime.create({ application })
await app.serve({ port: app.get(AppEnv).PORT })
TaskとTriggerを追加する
Task semanticsは@loutrejs/loutre/tasksが提供します。
import { defineApplication, defineModule } from '@loutrejs/loutre'
import { fixedDelay, task } from '@loutrejs/loutre/tasks'
export const cleanup = task<void, void>({
name: 'cleanup',
factory: () => async () => {
console.log('cleanup')
},
})
export const heartbeat = fixedDelay({
name: 'heartbeat',
delay: 5_000,
immediate: true,
task: cleanup,
})
const WorkerModule = defineModule({
executions: [heartbeat],
})
export default defineApplication({ modules: [WorkerModule] })
Triggerだけを登録すれば十分です。Tasks Extensionが参照先Taskを宣言し、CoreがそのTaskをcanonical Application Modelへ自動で含めます。
Taskのmanual executionとTrigger起動はExtension-owned Host APIから行います。
import { nodeRuntime } from '@loutrejs/node'
import application, { cleanup } from './app.js'
const app = await nodeRuntime.create({ application })
await app.tasks.run(cleanup)
await app.tasks.start()
Queue consumer
Queue descriptorとconsumerも@loutrejs/loutre/tasksから提供されます。BullMQのようなdriverは同じModuleのProviderとして宣言します。
import { defineModule } from '@loutrejs/loutre'
import { consume, queue, task } from '@loutrejs/loutre/tasks'
import { bindBullMqQueue } from '@loutrejs/bullmq'
import { z } from 'zod'
const orders = queue({
name: 'orders',
payload: z.object({ id: z.string() }),
})
const processOrder = task<{ id: string }, void>({
name: 'orders.process',
factory:
() =>
async ({ id }) =>
console.log(id),
})
const orderConsumer = consume({
name: 'orders.consumer',
queue: orders,
task: processOrder,
})
const QueueModule = defineModule({
providers: [
bindBullMqQueue(orders, {
connection: { host: '127.0.0.1', port: 6379 },
}),
],
executions: [processOrder, orderConsumer],
})
Application Modelを確認する
npm exec loutre -- check --entry src/app.ts
npm exec loutre -- graph modules --entry src/app.ts
npm exec loutre -- graph di --entry src/app.ts --format mermaid
npm exec loutre -- graph executions --entry src/app.ts --format json
npm exec loutre -- doctor --entry src/app.ts --runtime node
CLIはRuntimeと同じcompile済みApplication Modelを読みます。別のGraph compilerを持ちません。
OpenAPIを生成する
HTTP OpenAPI metadataはHTTP Extensionがprojectionします。
npm exec loutre -- openapi --entry src/app.ts --output openapi.json
KernelとExecution Extensionの境界はArchitectureを参照してください。
このガイドでは、小さなHTTP Applicationを作り、portableなKernel Hostからテストし、Node.jsへ接続します。
プロジェクトを作成する
npm create loutre@latest my-app cd my-appTargetにはNode.js、Bun、Cloudflare Workers、AWS Lambdaを選べます。
HTTP executionを定義する
HTTP semanticsはHTTP Extensionに置き、生成されたExecution DefinitionをModuleへ登録します。
src/greetings/contract.ts:import { http } from '@loutrejs/loutre/http' import { z } from 'zod' export const GreetingContract = http.contract({ greet: { method: 'GET', path: '/greetings/{name}', request: { params: { name: z.string().min(1), }, }, responses: { ok: { status: 200, body: z.object({ message: z.string() }), }, }, }, })src/greetings/service.ts:export class GreetingService { greet(name: string) { return { message: `こんにちは、${name}!` } } }src/greetings/controller.ts:import { inject } from '@loutrejs/loutre' import { http } from '@loutrejs/loutre/http' import { GreetingContract } from './contract.js' import { GreetingService } from './service.js' export const GreetingHttp = http.implementation({ name: 'GreetingHttp', contract: GreetingContract, factory: (greetings = inject(GreetingService)) => ({ async greet(ctx) { return ctx.response.ok({ body: greetings.greet(ctx.input.params.name), }) }, }), })src/app.ts:import { defineApplication, defineModule } from '@loutrejs/loutre' import { GreetingHttp } from './greetings/controller.js' import { GreetingService } from './greetings/service.js' const AppModule = defineModule({ providers: [GreetingService], executions: [GreetingHttp], }) export default defineApplication({ modules: [AppModule], })defineApplication()がcanonical Application Modelを構築します。HTTP routeの詳細はHTTP Extensionが所有し、Coreはprotocol固有の分岐を持ちません。静的Moduleと動的Module
引数のないModuleは
defineModule({ ... })で定義し、返されたinstanceをそのままmodulesやimportsへ渡します。設定が必要なModuleはfactoryを渡して、利用時に引数を指定します。const UsersModule = defineModule({ providers: [UsersService] }) const DatabaseModule = defineModule((options: { url: string }) => ({ providers: [provide(DatabaseUrl).useValue(options.url)], exports: [DatabaseUrl], })) const AppModule = defineModule({ imports: [UsersModule, DatabaseModule({ url: 'postgres://localhost/app' })], })同じinstanceの参照はGraph内で共有されます。動的factoryを明示的に呼ぶたびに別instanceが作られるので、共有したい場合は結果を変数に保存してください。旧形式の
defineModule(() => ({ ... }))()も利用できます。引数なしの旧templateを直接modulesやimportsへ渡す場合は、Model構築ごとに同じtemplateを一つのinstanceとして正規化します。異なるApplicationの構築間ではfactoryの結果を共有しません。portable Kernel Hostからテストする
import { bootstrapApplication } from '@loutrejs/loutre' import { bindHttpServer } from '@loutrejs/loutre/http' import { expect, it } from 'vitest' import application from './app.js' it('挨拶を返す', async () => { const app = await bootstrapApplication({ application, capabilities: [bindHttpServer({ runtime: 'test' })], }) try { const response = await app.http.fetch( new Request('http://localhost/greetings/Loutre'), ) expect(response.status).toBe(200) await expect(response.json()).resolves.toEqual({ message: 'こんにちは、Loutre!', }) } finally { await app.close('test-complete') } })bootstrapApplication()はKernelを初期化し、Application Modelに含まれるExtensionがcontributeしたHost APIだけを公開します。Node.jsへ接続する
src/main.ts:import { nodeRuntime } from '@loutrejs/node' import application from './app.js' const app = await nodeRuntime.create({ application }) await app.serve({ port: 3000 }){ "message": "こんにちは、Loutre!" }Application Definitionはportableなままです。platform固有のlistenerやcallback boundaryはRuntime adapterが所有します。
Environmentを追加する
import { defineEnv, defineModule } from '@loutrejs/loutre' import { z } from 'zod' export class AppEnv extends defineEnv( z.object({ PORT: z.coerce.number().int().positive().default(3000) }), ) {} const AppModule = defineModule({ environment: [AppEnv], executions: [GreetingHttp], })Application初期化後はvalidation済みの値を利用できます。
const app = await nodeRuntime.create({ application }) await app.serve({ port: app.get(AppEnv).PORT })TaskとTriggerを追加する
Task semanticsは
@loutrejs/loutre/tasksが提供します。import { defineApplication, defineModule } from '@loutrejs/loutre' import { fixedDelay, task } from '@loutrejs/loutre/tasks' export const cleanup = task<void, void>({ name: 'cleanup', factory: () => async () => { console.log('cleanup') }, }) export const heartbeat = fixedDelay({ name: 'heartbeat', delay: 5_000, immediate: true, task: cleanup, }) const WorkerModule = defineModule({ executions: [heartbeat], }) export default defineApplication({ modules: [WorkerModule] })Triggerだけを登録すれば十分です。Tasks Extensionが参照先Taskを宣言し、CoreがそのTaskをcanonical Application Modelへ自動で含めます。
Taskのmanual executionとTrigger起動はExtension-owned Host APIから行います。
import { nodeRuntime } from '@loutrejs/node' import application, { cleanup } from './app.js' const app = await nodeRuntime.create({ application }) await app.tasks.run(cleanup) await app.tasks.start()Queue consumer
Queue descriptorとconsumerも
@loutrejs/loutre/tasksから提供されます。BullMQのようなdriverは同じModuleのProviderとして宣言します。import { defineModule } from '@loutrejs/loutre' import { consume, queue, task } from '@loutrejs/loutre/tasks' import { bindBullMqQueue } from '@loutrejs/bullmq' import { z } from 'zod' const orders = queue({ name: 'orders', payload: z.object({ id: z.string() }), }) const processOrder = task<{ id: string }, void>({ name: 'orders.process', factory: () => async ({ id }) => console.log(id), }) const orderConsumer = consume({ name: 'orders.consumer', queue: orders, task: processOrder, }) const QueueModule = defineModule({ providers: [ bindBullMqQueue(orders, { connection: { host: '127.0.0.1', port: 6379 }, }), ], executions: [processOrder, orderConsumer], })Application Modelを確認する
npm exec loutre -- check --entry src/app.ts npm exec loutre -- graph modules --entry src/app.ts npm exec loutre -- graph di --entry src/app.ts --format mermaid npm exec loutre -- graph executions --entry src/app.ts --format json npm exec loutre -- doctor --entry src/app.ts --runtime nodeCLIはRuntimeと同じcompile済みApplication Modelを読みます。別のGraph compilerを持ちません。
OpenAPIを生成する
HTTP OpenAPI metadataはHTTP Extensionがprojectionします。
npm exec loutre -- openapi --entry src/app.ts --output openapi.jsonKernelとExecution Extensionの境界はArchitectureを参照してください。