Skip to content

Your First Worker

In this guide you will build a Stratal worker with a single GET /api/hello endpoint that returns a JSON greeting. By the end you will have a running worker with a controller, a root module, and an entry point.

Controllers handle incoming requests. Create src/hello.controller.ts:

import { Controller, IController, Route, RouterContext } from 'stratal/router'
import { z } from 'stratal/validation'
@Controller('/api/hello')
export class HelloController implements IController {
@Route({
response: z.object({ message: z.string() }),
})
index(ctx: RouterContext) {
return ctx.json({ message: 'Hello World' })
}
}

A few things to note:

  • @Controller('/api/hello') registers this class as a controller and sets its base path.

  • IController is the interface every controller implements. It defines convention-based method names that map to HTTP verbs automatically:

    MethodHTTP verbRoute
    index()GET/api/hello
    show()GET/api/hello/:id
    create()POST/api/hello
    update()PUT/api/hello/:id
    patch()PATCH/api/hello/:id
    destroy()DELETE/api/hello/:id

    You only implement the methods you need — here we define index() to handle GET /api/hello.

  • @Route configures the route. The response property is a Zod schema that validates the response body and feeds into automatic OpenAPI documentation.

  • RouterContext gives you access to the request, params, and helper methods like ctx.json().

If you have an AI agent (Claude Code, VS Code Copilot, Windsurf, etc.), install the Stratal skill so it understands these conventions out of the box:

Terminal window
npx skills add strataljs/stratal

With the skill installed, your AI agent knows how @Controller paths map to routes, how IController methods map to HTTP verbs, how @Route schemas feed into OpenAPI, and how modules wire everything together. It can generate controllers, services, modules, and DI bindings following these exact patterns — so you can describe what you want and let your agent build it.

Once your routes are running, you can also expose them as MCP tools so AI agents can discover and call your API endpoints directly:

Terminal window
# Preview available tools
npx quarry mcp:tools
# Start the MCP server
npx quarry mcp:serve

See AI Integration for client configuration and the full MCP setup.

Every Stratal application has a root module that declares which controllers (and later, providers) belong to the app. Create src/app.module.ts:

import { Module } from 'stratal/module'
import { HelloController } from './hello.controller'
@Module({
controllers: [HelloController],
})
export class AppModule {}

The @Module decorator accepts an options object with:

  • controllers — an array of controller classes to register.
  • providers — services and other injectable classes (covered in Dependency Injection).
  • imports — other modules to compose into this one (covered in Modules).

For now, a single controller is all you need.

The entry point is the file Wrangler invokes when a request arrives. Create src/index.ts:

import 'reflect-metadata'
import { Stratal } from 'stratal'
import { AppModule } from './app.module'
export default new Stratal({ module: AppModule })
  • import 'reflect-metadata' must appear once at the top of your entrypoint. It enables the decorator metadata that Stratal’s dependency injection (powered by tsyringe) relies on.
  • Stratal is the framework entry point. It eagerly bootstraps the module system, router, and DI container.
  • The module option points to your root module.

Start the local development server:

Terminal window
npx wrangler dev

Once Wrangler is ready you will see output like:

⎔ Starting local server...
Ready on http://localhost:8787

Test your endpoint:

Terminal window
curl http://localhost:8787/api/hello

You should receive:

{ "message": "Hello World" }

Here is the path a request takes through your worker:

  1. Wrangler receives the HTTP request and hands it to the exported Stratal instance.
  2. Stratal passes the request to the router, which runs inside an initialized DI container.
  3. The router matches GET /api/hello to HelloController.index() using the convention-based mapping.
  4. The controller method runs and returns a JSON response.
  5. The response is sent back to the caller.

Real applications rarely put business logic directly in controllers. Stratal uses dependency injection to keep concerns separated. Let’s extract the greeting into a service.

Create src/hello.service.ts:

import { Transient } from 'stratal/di'
@Transient()
export class HelloService {
greet(name: string): string {
return `Hello, ${name}!`
}
}

@Transient() marks the class as injectable. By default it creates a new instance each time it is resolved.

Now update the controller to use the service. Replace the contents of src/hello.controller.ts:

import { Controller, IController, Route, RouterContext } from 'stratal/router'
import { z } from 'stratal/validation'
import { HelloService } from './hello.service'
@Controller('/api/hello')
export class HelloController implements IController {
constructor(private readonly helloService: HelloService) {}
@Route({
response: z.object({ message: z.string() }),
})
index(ctx: RouterContext) {
const message = this.helloService.greet('World')
return ctx.json({ message })
}
}

Register the service as a provider in src/app.module.ts:

import { Module } from 'stratal/module'
import { HelloController } from './hello.controller'
import { HelloService } from './hello.service'
@Module({
controllers: [HelloController],
providers: [HelloService],
})
export class AppModule {}

Restart the dev server and hit the endpoint again — the response is now {"message":"Hello, World!"}, produced by the injected service.

Your project should now look like this:

my-worker/
├── src/
│ ├── app.module.ts
│ ├── hello.controller.ts
│ ├── hello.service.ts
│ └── index.ts
├── package.json
├── tsconfig.json
└── wrangler.jsonc

From here you can explore: