Skip to main content

Getting Started with Cloudflare Workers

This guide walks you through building a Zelt application on Cloudflare Workers from scratch.

Prerequisites

  • Node.js v20 or higher (or Bun v1.0 or higher)
  • A package manager: pnpm (recommended), npm, or bun

Additional requirements for Cloudflare Workers:

Installation

pnpm add @zeltjs/core @zeltjs/adapter-cloudflare-workers
pnpm add -D wrangler @cloudflare/workers-types

Project Structure

my-app/
├── src/
│   ├── entry/
│   │   ├── controllers/    # HTTP endpoints
│   │   └── commands/       # CLI commands
│   ├── services/           # Business logic
│   ├── configs/            # Configuration classes
│   ├── app.ts              # Application definition
│   ├── cli.ts              # CLI entry point
│   └── main.ts             # HTTP server entry point
├── package.json
└── tsconfig.json
DirectoryPurpose
entry/External entry points (HTTP, CLI)
services/Business logic, injected via DI
configs/Environment variables and settings

For Cloudflare Workers, also add wrangler.toml at the project root.

Hello World

Step 1: Create the Controller

Controllers handle incoming HTTP requests and return responses. Each controller is a class decorated with @Controller that defines a route prefix.

Create src/entry/controllers/hello.controller.ts:

@Controller('/hello')
export class HelloController {
  @Get('/:name')
  greet(name = pathParam('name')) {
    return { message: `Hello, ${name}!` };
  }
}
  • @Controller('/hello') — Sets the base path for all routes in this controller
  • @Get('/:name') — Handles GET requests to /hello/:name
  • pathParam('name') — Extracts the name parameter from the URL path

Step 2: Create the Application

Create src/app.ts to wire up your controllers:

export const app = createApp([http({
    controllers: [HelloController],
  })]);

Step 3: Create the Worker Entry Point

Create src/index.ts as the Cloudflare Workers entry point:

const workers = await onCloudflareWorkers(app);

export default { fetch: workers.fetch };

The onCloudflareWorkers() function is async and prepares your app for the Workers runtime. The returned object contains the fetch handler along with other utilities like shutdown and get for accessing services. By default, it uses lazy initialization (warmup: false) — controllers are resolved on the first request rather than at startup. This optimizes cold start times in serverless environments.

Step 4: Configure Wrangler

Create wrangler.toml:

name = "my-zelt-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]

[vars]
API_HOST = "https://api.example.com"

Step 5: Configure TypeScript

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "experimentalDecorators": true,
    "skipLibCheck": true,
    "types": ["@cloudflare/workers-types"]
  },
  "include": ["src"]
}

Step 6: Run Locally

npx wrangler dev

Visit http://localhost:8787/hello/world to see:

{ "message": "Hello, world!" }

Configuration

Environment Variables

In Cloudflare Workers, environment variables are configured in wrangler.toml and accessed via Env.

@Controller('/config')
export class ConfigController {
  constructor(private env = inject(Env)) {}

  @Get('/api-host')
  getApiHost() {
    return { apiHost: this.env.getString('API_HOST', 'localhost') };
  }
}
export const app = createApp([http({
    controllers: [ConfigController],
  })]);

Important: onCloudflareWorkers() automatically registers the env adaptor, so inject(Env) reads environment variables from the Workers runtime (cloudflare:workers module) without any extra configuration.

Secrets

For sensitive values, use Wrangler secrets instead of [vars]:

npx wrangler secret put DATABASE_URL

Access them the same way via Env:

  get connectionUrl() {
    return this.env.getString('DATABASE_URL', '');
  }
}

Services

Services work identically to Node.js. Use @Injectable to mark a class as a service.

@Injectable()
export class GreetingService {
  greet(name: string): string {
    return `Hello, ${name}!`;
  }
}

Inject into controllers:

@Controller('/hello')
export class HelloController {
  constructor(private greetingService = inject(GreetingService)) {}

  @Get('/:name')
  greet(name = pathParam('name')) {
    return { message: this.greetingService.greet(name) };
  }
}

Deploy

Deploy your worker to Cloudflare's global network:

npx wrangler deploy

Your worker will be available at https://my-zelt-worker.<your-subdomain>.workers.dev.

Advanced: Warmup Option

By default, onCloudflareWorkers() uses lazy initialization (warmup: false) to minimize cold start time. Controllers are resolved on the first request.

If you prefer to resolve all controllers at initialization (useful for debugging or when cold start time is less critical), set warmup: true:

const workers = await onCloudflareWorkers(app, { warmup: true });

export default { fetch: workers.fetch };
OptionBehaviorUse Case
warmup: false (default)Controllers resolved on first requestOptimized cold starts
warmup: trueAll controllers resolved at initializationDebugging, warm environments

What's Next?

Now that you have a basic worker running, explore more features: