Getting Started

This guide takes you from an empty folder to a running Benzene service in about five minutes — no cloud account required. You'll build a small HTTP service locally with Express, then, if you want, take the exact same message handler to AWS Lambda or Azure Functions without changing a line of it.

If you already know you're deploying to a specific platform, you can jump straight to AWS Lambda Setup or Azure Functions Setup — but starting here first is the quickest way to see how Benzene fits together.

TypeScript port. This is the TypeScript port of Benzene. It mirrors the .NET library's shape as closely as the language allows; where the two differ, the README's Porting conventions explain why.

What you'll build

A single endpoint, POST /hello, that takes a JSON body {"name":"world"} and returns a JSON greeting. It's deliberately tiny so the focus stays on the moving parts you'll reuse in every Benzene service: a message handler, a topic, and the middleware pipeline that connects a transport to your handler.

Prerequisites

That's it — the local walkthrough needs nothing else installed.

The core idea in 60 seconds

Benzene separates what your service does from how it's invoked:

Because only the transport pipeline changes between hosts, the handler you write below runs unchanged on every platform Benzene supports. See Message Handlers and Middleware for the full picture.

1. Create the project

mkdir hello-benzene && cd hello-benzene
npm init -y
npm pkg set type=module

Setting type=module makes this an ES-module project, which Benzene's packages require.

2. Install the packages

Pre-release. The @benzene/* packages aren't published to npm yet, so the npm install below won't resolve them from the public registry today. Until they're published, clone benzene-typescript and build your service inside its npm workspace (every @benzene/* package resolves locally there), or add them as file: dependencies pointing at your checkout. The command below is the package set you'll depend on once they ship — the rest of this guide is unchanged either way.

npm install @benzene/express @benzene/core-message-handlers @benzene/http @benzene/results \
  @benzene/abstractions @benzene/abstractions-message-handlers express
npm install --save-dev typescript tsx @types/express

@benzene/express is the Express host adapter; it brings in the middleware pipeline and message-handler infrastructure. The @benzene/* abstraction packages supply the types your handler references, and tsx lets you run TypeScript directly without a build step.

No tsconfig.json is needed for this quickstart: Benzene uses the standard (TC39 stage-3) decorators, which tsx runs directly — you do not need experimentalDecorators. Your editor may still want a tsconfig.json for IntelliSense; a plain { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true } } is enough.

3. Write a message handler

Create src/HelloWorldHandler.ts. This is where your logic lives — and the only file you'd carry over verbatim if you later moved to Lambda or Azure Functions:

import { IBenzeneResultOf } from '@benzene/abstractions';
import { IMessageHandler } from '@benzene/abstractions-message-handlers';
import { message } from '@benzene/core-message-handlers';
import { httpEndpoint } from '@benzene/http';
import { BenzeneResult } from '@benzene/results';

// Payloads are classes, not interfaces: the runtime recovers the erased request type from its
// constructor (for topic/schema keying), which an interface can't provide.
export class HelloWorldRequest {
  name?: string;
}

export class HelloWorldResponse {
  message?: string;
}

@httpEndpoint('POST', '/hello')
@message('hello:world', { requestType: HelloWorldRequest, responseType: HelloWorldResponse })
export class HelloWorldHandler implements IMessageHandler<HelloWorldRequest, HelloWorldResponse> {
  handleAsync(request: HelloWorldRequest): Promise<IBenzeneResultOf<HelloWorldResponse>> {
    const response = new HelloWorldResponse();
    response.message = `Hello ${request.name ?? 'world'}!`;
    return Promise.resolve(BenzeneResult.ok(response));
  }
}

Two decorators do the wiring:

The return type — Promise<IBenzeneResultOf<HelloWorldResponse>> — is the response wrapped in a result, which carries success/failure status alongside the payload. BenzeneResult.ok(...) is the success case.

A note on request binding. Benzene binds the JSON request body onto your request object, so POST /hello with {"name":"world"} populates request.name. Unlike .NET, the TypeScript port does not bind path/query segments onto a bodyless request (it can't default-construct the erased DTO the way C#'s Activator.CreateInstance does) — so this guide uses a POST body rather than the .NET guide's GET /hello/{name}. Read values a client sends in the body.

4. Wire up the Express host

Create src/index.ts:

import express from 'express';
import { useMessageHandlers } from '@benzene/core-message-handlers';
import { benzene } from '@benzene/express';
import { HelloWorldHandler } from './HelloWorldHandler.js';

const app = express();

// Mount Benzene BEFORE any body parser so it reads the raw request body.
// It turns each matching request into a message and routes it to a handler by topic.
app.use(benzene((pipeline) => useMessageHandlers(pipeline, HelloWorldHandler)));

app.listen(3000, () => console.log('Listening on http://localhost:3000'));

There's a single Benzene call: benzene((pipeline) => useMessageHandlers(pipeline, HelloWorldHandler)) returns Express middleware that inserts Benzene into the request pipeline. useMessageHandlers(pipeline, …) is the step that routes a matched request to its handler — pass every handler class you want served.

The Benzene middleware only responds to requests that match one of your @httpEndpoint routes — anything it doesn't recognise falls through to the rest of the Express app, so it coexists cleanly with your existing routes, static files, or health-check endpoints.

5. Run it

npx tsx src/index.ts

In another terminal:

curl -X POST http://localhost:3000/hello -H 'content-type: application/json' -d '{"name":"world"}'
{"message":"Hello world!"}

That's a complete Benzene service. The request arrived over HTTP, Benzene mapped POST /hello to the hello:world topic, bound the JSON body onto HelloWorldRequest, invoked your handler, and serialised the result back as JSON.

What just happened

POST /hello  {"name":"world"}
      │
      ▼
@httpEndpoint route match  ──►  topic "hello:world"
      │
      ▼
HelloWorldHandler.handleAsync(request)
      │
      ▼
BenzeneResult.ok(response)  ──►  200  {"message":"Hello world!"}

The handler in the middle never touched an Express req/res. Swap the transport pipeline in src/index.ts for an AWS Lambda or Azure Functions one and the same handler runs there — that's the portability Benzene's hexagonal design buys you.

Next steps

Now that you have a service running, layer on the cross-cutting concerns and platforms you need — each is a small, self-contained addition:

For complete, runnable projects covering every transport, see the examples/ folder in the repository.