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
- Wrangler CLI
- A Cloudflare account (free tier available)
Installation
pnpm add @zeltjs/core @zeltjs/adapter-cloudflare-workers
pnpm add -D wrangler @cloudflare/workers-types
Project Structure
my-worker/
├── src/
│ ├── app.ts
│ ├── index.ts
│ └── controllers/
│ └── hello.controller.ts
├── package.json
├── tsconfig.json
└── wrangler.toml
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/controllers/hello.controller.ts:
import { Controller, Get, pathParam } from '@zeltjs/core';
@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/:namepathParam('name')— Extracts thenameparameter from the URL path
Step 2: Create the Application
Create src/app.ts to wire up your controllers:
import { createHttpApp } from '@zeltjs/core';
import { HelloController } from './controllers/hello.controller';
export const app = createHttpApp({
controllers: [HelloController],
});
Step 3: Create the Worker Entry Point
Create src/index.ts as the Cloudflare Workers entry point:
import { onCloudflareWorkers } from '@zeltjs/adapter-cloudflare-workers';
import { app } from './app';
export default onCloudflareWorkers(app);
The onCloudflareWorkers() function wraps your app for the Workers runtime. By default, it uses lazy initialization — 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 EnvService.
import { Controller, Get, inject, EnvService } from '@zeltjs/core';
@Controller('/config')
export class ConfigController {
constructor(private env = inject(EnvService)) {}
@Get('/api-host')
getApiHost() {
return { apiHost: this.env.get('API_HOST') ?? 'localhost' };
}
}
Register EnvConfig in your app:
import { createHttpApp, EnvConfig } from '@zeltjs/core';
export const app = createHttpApp({
controllers: [ConfigController],
configs: [EnvConfig],
});
Important: When you register EnvConfig and use onCloudflareWorkers(), the adapter automatically replaces it with CloudflareWorkersEnvConfig. This reads environment variables from the Workers runtime (cloudflare:workers module) instead of process.env.
Secrets
For sensitive values, use Wrangler secrets instead of [vars]:
npx wrangler secret put DATABASE_URL
Access them the same way via EnvService:
const dbUrl = this.env.get('DATABASE_URL');
Services
Services work identically to Node.js. Use @Injectable to mark a class as a service.
import { Injectable } from '@zeltjs/core';
@Injectable()
export class GreetingService {
greet(name: string): string {
return `Hello, ${name}!`;
}
}
Inject into controllers:
import { Controller, Get, pathParam, inject } from '@zeltjs/core';
import { GreetingService } from '../services/greeting.service';
@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:
export default onCloudflareWorkers(app, { warmup: true });
| Option | Behavior | Use Case |
|---|---|---|
warmup: false (default) | Controllers resolved on first request | Optimized cold starts |
warmup: true | All controllers resolved at initialization | Debugging, warm environments |
What's Next?
Now that you have a basic worker running, explore more features:
- Controllers — Route handling and HTTP methods
- Services — Business logic and dependency injection
- Validation — Request body validation with Valibot
- Middleware — Request/response interceptors
- Configuration — Advanced configuration patterns