Skip to content

First Worker

Your first Worker responds to HTTP requests with JSON. You write it in 22 lines of F# and compile it with Fable. Then you run it on your own machine in workerd, Cloudflare's open-source Workers runtime.

  • You need The .NET SDK, Node.js with npm, curl, and NuGet packages
  • You get hello-worker on localhost:8787

Project Folder

Create the application anywhere convenient. It restores FSharp.CloudEdge.Runtime.Workers from the 0.1.* patch series on NuGet; no sibling library checkout is required. See Packages for installation guidance and links to the full package family.

  1. Open a terminal in the folder where you keep your projects.

  2. Create hello-worker and enter it.

    mkdir hello-worker
    cd hello-worker
    

Stay in hello-worker for the remaining commands.

Project File

Save this as hello-worker.fsproj. It declares Worker.fs, Fable.Core, and the Workers package from the 0.1.* patch series.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="Worker.fs" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Fable.Core" Version="5.2.0" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="FSharp.CloudEdge.Runtime.Workers" Version="0.1.*" />
  </ItemGroup>
</Project>

Fable.Core 5.2.0 matches the Workers package's dependency. NuGet restores the Xantham support packages transitively. The package includes the F# source needed by Fable.

Next to the project, add NuGet.Config to select the public feed. Package IDs and versions belong in PackageReference; the feed URL belongs here.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

Worker Source

Put the handler in Worker.fs. It responds to / with a JSON greeting and to /time with the current UTC time. For any other route it returns a 404.

module Worker

open System
open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers

[<ExportDefault>]
let worker: Workers.ExportedHandler<obj, obj, obj, obj> =
    Workers.ExportedHandler.Create(fetch = fun request _ _ ->
        let url = Workers.Exports.URL(U2.Case1 request.url)
        let response =
            match url.pathname with
            | "/" ->
                let name = url.searchParams.get "name" |> Option.defaultValue "world"
                Workers.Exports.Response.json {| greeting = $"Hello, {name}" |}
            | "/time" ->
                Workers.Exports.Response.json {| utc = DateTime.UtcNow.ToString "o" |}
            | _ ->
                let init = Workers.ResponseInit.Create(status = 404.)
                Workers.Exports.Response.json({| error = "Not found" |}, U2.Case2 init)
        U2.Case2 response)
  • [<ExportDefault>] declares worker as the default export of the JavaScript module. The Workers runtime passes each incoming request to the fetch handler of worker.
  • ExportedHandler.Create builds the export from a fetch function. The function's first argument is the request, and the two _ patterns discard the environment and the execution context. All four type arguments of ExportedHandler are obj, the F# type for any value.
  • Workers.Exports contains the globals your Worker can call, so Workers.Exports.URL and Workers.Exports.Response are JavaScript's URL and Response.
  • An anonymous record such as {| greeting = ... |} compiles to a plain JavaScript object, and Response.json serializes it as the response body.
  • U2.Case1 and U2.Case2 wrap a value as the first or second type of a TypeScript union. The URL constructor takes a U2<string, URL>, and the fetch function returns a U2<JS.Promise<Response>, Response>.

Fable Compilation

  1. Create a tool manifest to pin .NET tool versions for hello-worker.

    dotnet new tool-manifest
    
    The template "Dotnet local tool manifest file" was created successfully.
    
  2. Install Fable 5.13.0, the release that the tool manifest of FSharp.CloudEdge pins.

    dotnet tool install fable --version 5.13.0
    
    You can invoke the tool from this directory using the following commands: 'dotnet tool run fable' or 'dotnet fable'.
    
  3. Compile hello-worker.fsproj into the build folder.

    dotnet fable hello-worker.fsproj -o build
    

Fable processes Worker.fs and the sources supplied by its NuGet dependencies. Source counts and compile times vary with the package contents. build/Worker.js contains your handler, and the Fable library code it imports is in build/fable_modules.

Emitted JavaScript
import { defaultArg } from "./fable_modules/fable-library-js.5.13.0/Option.js";
import { concat } from "./fable_modules/fable-library-js.5.13.0/String.js";
import { utcNow, toString } from "./fable_modules/fable-library-js.5.13.0/Date.js";

export const worker = {
    fetch: (request, _arg, _arg_1) => {
        const url = new URL(request.url);
        const matchValue = url.pathname;
        switch (matchValue) {
            case "/": {
                const name = defaultArg(url.searchParams.get("name"), "world");
                return globalThis.Response.json({
                    greeting: concat("Hello, ", name),
                });
            }
            case "/time":
                return globalThis.Response.json({
                    utc: toString(utcNow(), "o"),
                });
            default: {
                const init = {
                    status: 404,
                };
                return globalThis.Response.json({
                    error: "Not found",
                }, init);
            }
        }
    },
};

export default worker;

Bundled Module

esbuild resolves the imports in build/Worker.js and writes one ES module, dist/worker.js. You serve that bundle with workerd below and upload it on First Deploy.

  1. Add a package.json for the npm tools.

    npm init -y
    
  2. Install esbuild 0.28.2 and workerd 1.20260906.1, the releases that produced the output on this page. npm installs both with a binary for your platform.

    npm install --save-dev esbuild@0.28.2 workerd@1.20260906.1
    
    added 4 packages, and audited 5 packages in 2s
    
    found 0 vulnerabilities
    
  3. Bundle build/Worker.js.

    npx esbuild build/Worker.js --bundle --format=esm '--external:cloudflare:*' --outfile=dist/worker.js
    
      dist/worker.js  24.0kb
    
  • --bundle: esbuild copies the imported fable_modules code into the output.
  • --format=esm: the output is an ES module, with the same default export as build/Worker.js.
  • --external:cloudflare:*: esbuild leaves cloudflare: imports in place, for workerd and Cloudflare to resolve. A Durable Object class, for example, extends a base class from cloudflare:workers.
  • --outfile: the path of the bundle.

Runtime Configuration

workerd reads its settings from a Cap'n Proto text file. Put this one in hello-worker as config.capnp.

using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [(name = "hello-worker", worker = .helloWorker)],
  sockets = [(address = "localhost:8787", service = "hello-worker")],
);

const helloWorker :Workerd.Worker = (
  modules = [(name = "worker.js", esModule = embed "dist/worker.js")],
  compatibilityDate = "2026-09-06",
);
  • using Workerd loads the configuration schema built into workerd.
  • const config is the configuration that workerd serve starts from.
  • services defines one service, hello-worker, whose Worker is the helloWorker constant.
  • With sockets, workerd listens on port 8787 of localhost and passes requests to that service. HTTP is the default protocol for a socket.
  • const helloWorker defines the Worker.
  • modules lists one ES module, worker.js, and embed reads its contents from dist/worker.js.
  • compatibilityDate is required. With it, you opt into the runtime changes up to that day. 2026-09-06 is the date in 5.20260906.1, the version of @cloudflare/workers-types used to generate the Workers library.

workerd exits at startup when the compatibility date is later than the newest date it supports, and the error message states that date. Cloudflare explains the scheme in Compatibility dates.

Local Run

  1. Start workerd. With --watch, it reloads the Worker after each new bundle.

    npx workerd serve config.capnp --watch
    

    workerd prints nothing at startup and keeps serving until you stop it.

  2. In a second terminal, call the Worker.

    curl http://localhost:8787/
    
    {"greeting":"Hello, world"}
    
  3. Add a name to the query string. Quote the URL, because ? is a wildcard character in your shell.

    curl 'http://localhost:8787/?name=Ada'
    
    {"greeting":"Hello, Ada"}
    
  4. Ask for the time.

    curl http://localhost:8787/time
    
    {"utc":"2026-09-29T21:16:55.773Z"}
    
  5. Try any other path. With -i, curl also prints the status line and headers.

    curl -i http://localhost:8787/nope
    
    HTTP/1.1 404 Not Found
    Content-Length: 21
    Content-Type: application/json
    
    {"error":"Not found"}
    

Edit Loop

In watch mode, Fable keeps the project loaded and recompiles whenever you save Worker.fs. With --runWatch, Fable starts esbuild after each compile, and workerd reloads the new bundle.

  1. In a third terminal, go to hello-worker and start the watch.

    dotnet fable watch hello-worker.fsproj -o build --runWatch npx esbuild build/Worker.js --bundle --format=esm '--external:cloudflare:*' --outfile=dist/worker.js
    

    The initial compile takes as long as before. Fable prints Watching .. when it is ready.

  2. In Worker.fs, change Hello to Howdy and save. Fable reports the recompile:

    Fable compilation finished in 127ms
    

    workerd reports the reload:

    Reloading due to config change...
    
  3. Call the Worker again.

    curl http://localhost:8787/
    
    {"greeting":"Howdy, world"}
    

Stop Fable and workerd with Ctrl+C.

Next Step

Edit this page