Getting Started with Cloudflare Workers
This guide walks you through building a Zelt application on Cloudflare Workers from scratch.
Prerequisites
Additional requirements for Cloudflare Workers:
- 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-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
| Directory | Purpose |
|---|---|
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/:namepathParam('name')— Extracts thenameparameter 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 };
| 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