stratal
npx skills add https://github.com/strataljs/stratal --skill stratal
Agent 安装分布
Skill 文档
Stratal Framework
Stratal is a modular Cloudflare Workers framework. It provides dependency injection (tsyringe), Hono-based routing with automatic OpenAPI 3.0 docs, queue consumers, cron jobs, email, storage, caching, and i18n.
Key constraints:
- ESM-only (
"type": "module") - Build with
tsconly â never esbuild/tsup (tsyringe requiresemitDecoratorMetadata) experimentalDecoratorsandemitDecoratorMetadatamust be enabled in tsconfig- Two packages:
stratal(core framework),@stratal/testing(test utilities) - Always import Zod from
stratal/validation, never fromzoddirectly
Quick Start
Worker Entry Point
import { type ApplicationConfig } from 'stratal'
import { StratalWorker } from 'stratal/worker'
import { AppModule } from './app.module'
export default class Backend extends StratalWorker {
protected configure(): ApplicationConfig {
return {
module: AppModule,
}
}
}
StratalWorker extends Cloudflare’s WorkerEntrypoint. It handles HTTP fetch, queue batches, and scheduled cron triggers automatically.
Root Module
import { Module } from 'stratal/module'
import { UsersModule } from './users/users.module'
@Module({
imports: [UsersModule],
})
export class AppModule {}
Controller
import { Controller, Route, type IController, type RouterContext } from 'stratal/router'
import { z } from 'stratal/validation'
import { inject } from 'stratal/di'
import { Transient } from 'stratal/di'
const userSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
})
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
const USER_TOKENS = {
UserService: Symbol.for('UserService'),
}
@Controller('/api/v1/users', { tags: ['Users'] })
export class UsersController implements IController {
constructor(
@inject(USER_TOKENS.UserService) private readonly userService: UserService,
) {}
@Route({ response: z.array(userSchema) })
async index(ctx: RouterContext) {
const users = await this.userService.findAll()
return ctx.json(users)
}
@Route({
params: z.object({ id: z.string().uuid() }),
response: userSchema,
})
async show(ctx: RouterContext) {
const user = await this.userService.findById(ctx.param('id'))
return ctx.json(user)
}
@Route({ body: createUserSchema, response: userSchema })
async create(ctx: RouterContext) {
const body = await ctx.body<{ name: string; email: string }>()
const user = await this.userService.create(body)
return ctx.json(user, 201)
}
@Route({
params: z.object({ id: z.string().uuid() }),
body: createUserSchema.partial(),
response: userSchema,
})
async update(ctx: RouterContext) {
const body = await ctx.body()
const user = await this.userService.update(ctx.param('id'), body)
return ctx.json(user)
}
@Route({
params: z.object({ id: z.string().uuid() }),
response: z.object({ success: z.boolean() }),
})
async destroy(ctx: RouterContext) {
await this.userService.delete(ctx.param('id'))
return ctx.json({ success: true })
}
}
Method Name to HTTP Mapping
| Method | HTTP | Path Suffix | Status |
|---|---|---|---|
index |
GET | / |
200 |
show |
GET | /:id |
200 |
create |
POST | / |
201 |
update |
PUT | /:id |
200 |
patch |
PATCH | /:id |
200 |
destroy |
DELETE | /:id |
200 |
Controllers must implement IController. Method names must match the table above â the framework auto-derives the HTTP method, path, and status code from the method name.
For non-RESTful routes (wildcards, custom patterns), implement a handle() method instead.
[!reference] For full RouteConfig, ControllerOptions, RouterContext API, and OpenAPI setup, see routing.md
Dependency Injection
Tokens
Always use Symbol-based tokens, never string tokens:
const TOKENS = {
UserService: Symbol.for('UserService'),
UserRepository: Symbol.for('UserRepository'),
}
Services
Decorate injectable services with @Transient():
import { Transient } from 'stratal/di'
import { inject } from 'stratal/di'
@Transient()
export class UserService {
constructor(
@inject(TOKENS.UserRepository) private readonly repo: UserRepository,
) {}
}
Provider Types
Register providers in @Module({ providers: [...] }):
// Class provider (shorthand â class used as both token and implementation)
providers: [UserService]
// Class provider with explicit token
{ provide: TOKENS.UserService, useClass: UserService }
// Class provider with scope
{ provide: TOKENS.UserService, useClass: UserService, scope: Scope.Singleton }
// Value provider
{ provide: TOKENS.Config, useValue: { apiUrl: 'https://...' } }
// Factory provider
{ provide: TOKENS.Formatter, useFactory: (config) => new Formatter(config), inject: [TOKENS.Config] }
// Alias provider
{ provide: TOKENS.IUserService, useExisting: UserService }
Scopes
import { Scope } from 'stratal/di'
Scope.Transient // New instance per resolution (default)
Scope.Singleton // Single instance globally
Scope.Request // New instance per HTTP request
StratalEnv Augmentation
Run npx wrangler types to auto-generate Cloudflare.Env from your wrangler.jsonc, then extend it:
// src/types/env.ts
declare module 'stratal' {
interface StratalEnv extends Cloudflare.Env {}
}
This keeps your env types in sync with wrangler.jsonc automatically. The base StratalEnv already includes ENVIRONMENT: string and CACHE: KVNamespace.
[!reference] For Container API, conditional bindings, request scope, and dynamic modules, see modules-and-di.md
Guards and Middleware
Guards
Guards implement CanActivate and protect routes:
import { type CanActivate, UseGuards } from 'stratal/guards'
import { type RouterContext } from 'stratal/router'
import { Transient } from 'stratal/di'
@Transient()
export class AuthGuard implements CanActivate {
async canActivate(context: RouterContext): Promise<boolean> {
const token = context.header('Authorization')
return !!token
}
}
// Apply to entire controller
@Controller('/api/v1/admin')
@UseGuards(AuthGuard)
export class AdminController implements IController { ... }
// Apply to single route method
@UseGuards(AuthGuard)
async create(ctx: RouterContext) { ... }
Middleware
Modules implement MiddlewareConfigurable to apply middleware:
import { type MiddlewareConfigurable, type MiddlewareConsumer, type Middleware } from 'stratal/middleware'
import { type RouterContext } from 'stratal/router'
@Transient()
export class LoggingMiddleware implements Middleware {
async handle(ctx: RouterContext, next: () => Promise<void>): Promise<void> {
console.log(`${ctx.c.req.method} ${ctx.c.req.url}`)
await next()
}
}
@Module()
export class AppModule implements MiddlewareConfigurable {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(LoggingMiddleware)
.exclude({ path: '/health', method: 'get' })
.forRoutes('*')
}
}
forRoutes() accepts '*' (all), controller classes, or { path, method } objects.
Queue Consumers and Cron Jobs
Queue Consumers
import { Transient } from 'stratal/di'
import { type IQueueConsumer, type QueueMessage } from 'stratal/queue'
interface OrderPayload {
orderId: string
amount: number
}
@Transient()
export class OrderCreatedConsumer implements IQueueConsumer<OrderPayload> {
readonly messageTypes = ['order.created']
async handle(message: QueueMessage<OrderPayload>): Promise<void> {
const { orderId, amount } = message.payload
// Process the order
}
async onError(error: Error, message: QueueMessage<OrderPayload>): Promise<void> {
console.error(`Failed to process order: ${message.payload.orderId}`, error)
}
}
Register consumers in @Module({ consumers: [OrderCreatedConsumer] }).
Cron Jobs
import { Transient } from 'stratal/di'
import { type CronJob } from 'stratal/cron'
@Transient()
export class DailyReportJob implements CronJob {
readonly schedule = '0 2 * * *' // Daily at 2 AM UTC
async execute(controller: ScheduledController): Promise<void> {
// Generate report
}
async onError(error: Error): Promise<void> {
console.error('Report generation failed', error)
}
}
Register jobs in @Module({ jobs: [DailyReportJob] }).
Also add matching cron triggers in wrangler.jsonc.
[!reference] For QueueMessage interface, QueueModule setup, EmailModule config, and dispatch patterns, see queues-cron-email.md
Built-in Modules
| Module | Import | Setup | Purpose |
|---|---|---|---|
| OpenAPIModule | stratal/openapi |
.forRoot({ info, securitySchemes }) |
Automatic API docs at /api/docs |
| ConfigModule | stratal/config |
.forRoot({ load: [dbConfig] }) |
Typed config with registerAs() |
| CacheModule | stratal/cache |
auto-registered | KV-backed caching via CACHE binding |
| EmailModule | stratal/email |
.forRoot({ provider, from, queue }) |
Resend or SMTP email |
| StorageModule | stratal/storage |
.forRoot({ storage, defaultStorageDisk }) |
S3-compatible file storage |
| I18nModule | stratal/i18n |
.forRoot({ defaultLocale, messages }) |
Type-safe translations |
| QueueModule | stratal/queue |
.forRootAsync(...) |
Queue producer/consumer |
[!reference] For detailed module configuration, see config-cache-storage-i18n.md
Error Handling
Stratal uses typed errors with numeric codes and i18n-translatable messages:
import { ApplicationError, type ErrorCode } from 'stratal/errors'
export class UserNotFoundError extends ApplicationError {
constructor(userId: string) {
super('errors.user.not_found', 4000, { userId })
}
}
Error code ranges by domain:
| Range | Domain |
|---|---|
| 1000-1999 | Validation |
| 2000-2999 | Database |
| 3000-3099 | Authentication |
| 3100-3199 | Authorization |
| 4000-4999 | Resource |
| 9000-9999 | System/Internal |
The built-in GlobalErrorHandler catches ApplicationError subclasses and returns structured JSON with translated messages. Throw from controllers or services â the handler will catch it.
Testing
Install @stratal/testing as a dev dependency:
import { Test, type TestingModule } from '@stratal/testing'
describe('UsersController', () => {
let module: TestingModule
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [UsersModule],
}).compile()
})
afterEach(async () => {
await module.close()
})
it('lists users', async () => {
const response = await module.http.get('/api/v1/users').send()
response.assertOk()
response.assertJsonStructure(['id', 'name', 'email'])
})
it('creates a user', async () => {
const response = await module.http
.post('/api/v1/users')
.withBody({ name: 'Alice', email: 'alice@example.com' })
.asJson()
.send()
response.assertCreated()
response.assertJsonPath('name', 'Alice')
})
})
Provider Overrides
const module = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(TOKENS.UserRepository)
.useValue(mockRepository)
.compile()
Resolve Services
const userService = module.get<UserService>(TOKENS.UserService)
[!reference] For TestHttpClient API, assertion methods, FakeStorageService, and test patterns, see testing.md
Sub-path Imports
| Import | Exports |
|---|---|
stratal |
Application, StratalEnv, Constructor, ApplicationConfig |
stratal/di |
Container, inject, Transient, Scope, DI_TOKENS, CONTAINER_TOKEN |
stratal/router |
Controller, Route, IController, RouterContext, UseGuards, ROUTER_TOKENS |
stratal/validation |
z (Zod), ZodType, ZodObject â always use this, not zod directly |
stratal/errors |
ApplicationError, ErrorCode, ERROR_CODES |
stratal/i18n |
I18nModule, I18nService, I18N_TOKENS, MessageKeys |
stratal/cache |
CacheModule, CacheService, CACHE_TOKENS |
stratal/config |
ConfigModule, registerAs, InferConfigType, CONFIG_TOKENS |
stratal/logger |
LoggerService, LOGGER_TOKENS, LogLevel |
stratal/queue |
QueueModule, IQueueConsumer, QueueMessage, QUEUE_TOKENS |
stratal/cron |
CronJob |
stratal/email |
EmailModule, EMAIL_TOKENS |
stratal/storage |
StorageModule, StorageService, STORAGE_TOKENS |
stratal/guards |
CanActivate, UseGuards |
stratal/middleware |
Middleware, MiddlewareConfigurable, MiddlewareConsumer |
stratal/module |
Module, ModuleOptions, DynamicModule, OnInitialize, OnShutdown, ModuleContext |
stratal/worker |
StratalWorker |
Best Practices
- One module per domain feature â e.g.,
UsersModule,OrdersModule,NotificationsModule - Symbol-based tokens with descriptive names:
Symbol.for('UserService'), not string tokens - Use
registerAs()for typed configuration namespaces - Controllers implement
IControllerâ ensures type-safe method signatures - Zod schemas for all request/response â define once, get validation + OpenAPI docs
@Transient()on all injectable services â required for tsyringe metadata- Import Zod from
stratal/validationâ ensures OpenAPI compatibility layer
Anti-Patterns
- Do NOT use esbuild or tsup â
emitDecoratorMetadatais not supported; build must usetsc - Do NOT use string DI tokens â always use
Symbol.for('...')for token uniqueness - Do NOT import
zoddirectly â usestratal/validationwhich wraps Zod with OpenAPI extensions - Do NOT forget
@Transient()on services â without it, tsyringe cannot resolve constructor metadata - Do NOT forget
IControllerinterface â controllers must implement it for type safety - Do NOT forget the
CACHEKV binding â needed inwrangler.jsoncif you useCacheService - Do NOT use
ctx.req.valid('json')in controllers â usectx.body<T>()which returns pre-validated data - Do NOT register providers outside of modules â always use
@Module({ providers: [...] })
[!reference] For project setup (wrangler.jsonc, tsconfig, env typing), see project-setup.md