Quick start
From an empty folder to a deployed URL shortener on KV.
On this page
A URL shortener in seven steps
-
Create the project and install.
@moku-labs/coreand@moku-labs/commonare peer dependencies.wrangleris an optional peer:deployandclirun it as a subprocess.@cloudflare/workers-typesgives the runtime types such asExecutionContext.install mkdir shortener && cd shortener bun init -y bun add @moku-labs/worker @moku-labs/core @moku-labs/common bun add -d wrangler @cloudflare/workers-types
-
Add the runtime types. List the Cloudflare package in
types. The types are ambient, so you never import them.tsconfig.json { "compilerOptions": { "module": "Preserve", "moduleResolution": "bundler", "strict": true, "types": ["@cloudflare/workers-types"] } } -
Write the shared config. Both apps below read it, so the Worker and the deploy cannot disagree on a name or a binding.
-
Compose the Worker app.
bindingsandserverare defaults. You addkvPluginand declare the routes as data. -
Write the Worker entry. No plugin produces the default export. You assemble it and hand each request to
app.server. -
Write the deploy script. This second app adds the Node-only plugins. Your Worker never calls it.
nodeCompatis required for every Worker on this package.deploydepends on all five resource plugins, so all five are listed. The ones without config contribute nothing. -
Run it locally, then deploy. The first deploy asks for a Cloudflare token and shows which one to create. See Build & deploy for the token and for CI.
The files of steps 3 to 6:
export const worker = { name: "shortener", compatibilityDate: "2026-06-17" };
export const kv = { links: { name: "shortener-links", binding: "LINKS" } };nameis the Cloudflare namespace.bindingis the key onenv.
import { createApp, endpoint, kvPlugin } from "@moku-labs/worker";
import { kv, worker } from "./config";
export const app = createApp({
plugins: [kvPlugin],
config: worker,
pluginConfigs: {
kv,
server: {
endpoints: [
endpoint("/links/{slug}").put(async ({ params, request, env, require }) => {
await require(kvPlugin).put(env, params.slug, await request.text());
return new Response(null, { status: 204 });
}),
endpoint("/{slug}").get(async ({ params, env, require }) => {
const target = await require(kvPlugin).get(env, params.slug);
return target === null ? new Response("Not Found", { status: 404 }) : Response.redirect(target, 302);
})
]
}
}
});envis the first argument of every call. Nothing stores it.
import { app } from "./app";
export default {
fetch: (request: Request, env: Record<string, unknown>, ctx: ExecutionContext) =>
app.server.handle(request, env, ctx)
} satisfies ExportedHandler;import {
cliPlugin,
createApp,
d1Plugin,
deployPlugin,
durableObjectsPlugin,
kvPlugin,
queuesPlugin,
storagePlugin
} from "@moku-labs/worker";
import { kv, worker } from "../src/config";
const tools = createApp({
plugins: [storagePlugin, kvPlugin, d1Plugin, queuesPlugin, durableObjectsPlugin, deployPlugin, cliPlugin],
config: worker,
pluginConfigs: {
kv,
deploy: {
entry: "src/worker.ts",
nodeCompat: true
}
}
});
if (process.argv[2] === "deploy") await tools.cli.deploy();
else await tools.cli.dev();- Becomes
mainin the generated wrangler config. - Writes
compatibility_flags: ["nodejs_compat"]. Without it the Worker fails at start.
Add two scripts, so wrangler from node_modules/.bin is on the PATH:
{
"scripts": {
"dev": "bun scripts/cli.ts dev",
"deploy": "bun scripts/cli.ts deploy"
}
}bun run dev curl -X PUT localhost:8787/links/moku -d https://moku-labs.com curl -i localhost:8787/moku HTTP/1.1 302 Found Location: https://moku-labs.com/
bun run dev writes wrangler.jsonc and starts wrangler dev on port 8787. bun run deploy lists
what already exists in your account, asks before it creates the rest, and asks again before it ships:
bun run deployIt prints a summary with the URL of the Worker, for example https://shortener.<your-subdomain>.workers.dev.
nodeCompatis not optional. The package is one file with top-levelnode:*imports, and wrangler keeps them in the bundle. Without the flag the Worker stops at start withNo such module "node:fs".- Two apps on purpose.
clireplaces the log sinks of its app, anddeployneeds five plugins your Worker may not use. The Worker app stays small. - List all five resource plugins for
deploy. With fewer,createAppthrowsPlugin "deploy" depends on "storage", but "storage" is not registered. - Run through
bun run.deployspawns the bare commandwrangler. It has to be on thePATH. - Do not list
bindingsPluginorserverPlugin. They are defaults. A second copy throwsDuplicate plugin name.
Related
- How it works: why
envis an argument. - server plugin: routes, guards and cron.
- kv plugin: namespaces and the full API.