|
| 1 | +--- |
| 2 | +order: 1 |
| 3 | +title: Get Started |
| 4 | +--- |
| 5 | + |
| 6 | +The Miniflare API allows you to dispatch events to workers without making actual HTTP requests, simulate connections between Workers, and interact with local emulations of storage products like [KV](/storage/kv), [R2](/storage/r2), and [Durable Objects](/storage/durable-objects). This makes it great for writing tests, or other advanced use cases where you need finer-grained control. |
| 7 | + |
| 8 | +## Installation |
| 9 | + |
| 10 | +Miniflare is installed using `npm` as a dev dependency: |
| 11 | + |
| 12 | +```sh |
| 13 | +$ npm install -D miniflare |
| 14 | +``` |
| 15 | + |
| 16 | +## Usage |
| 17 | + |
| 18 | +In all future examples, we'll assume Node.js is running in ES module mode. You |
| 19 | +can do this by setting the `type` field in your `package.json`: |
| 20 | + |
| 21 | +```json title=package.json |
| 22 | +{ |
| 23 | + ... |
| 24 | + "type": "module" |
| 25 | + ... |
| 26 | +} |
| 27 | +``` |
| 28 | + |
| 29 | +To initialise Miniflare, import the `Miniflare` class from `miniflare`: |
| 30 | + |
| 31 | +```js |
| 32 | +import { Miniflare } from "miniflare"; |
| 33 | + |
| 34 | +const mf = new Miniflare({ |
| 35 | + modules: true, |
| 36 | + script: ` |
| 37 | + export default { |
| 38 | + async fetch(request, env, ctx) { |
| 39 | + return new Response("Hello Miniflare!"); |
| 40 | + } |
| 41 | + } |
| 42 | + `, |
| 43 | +}); |
| 44 | + |
| 45 | +const res = await mf.dispatchFetch("http://localhost:8787/"); |
| 46 | +console.log(await res.text()); // Hello Miniflare! |
| 47 | +await mf.dispose(); |
| 48 | +``` |
| 49 | + |
| 50 | +The [rest of these docs](/core/fetch) go into more detail on configuring |
| 51 | +specific features. |
| 52 | + |
| 53 | +### String and File Scripts |
| 54 | + |
| 55 | +Note in the above example we're specifying `script` as a string. We could've |
| 56 | +equally put the script in a file such as `worker.js`, then used the `scriptPath` |
| 57 | +property instead: |
| 58 | + |
| 59 | +```js |
| 60 | +const mf = new Miniflare({ |
| 61 | + scriptPath: "worker.js", |
| 62 | +}); |
| 63 | +``` |
| 64 | + |
| 65 | +### Watching, Reloading and Disposing |
| 66 | + |
| 67 | +Miniflare's API is primarily intended for testing use cases, where file watching isn't usually required. If you need to watch files, consider using a separate file watcher like [fs.watch()](https://nodejs.org/api/fs.html#fswatchfilename-options-listener) or [chokidar](https://github.com/paulmillr/chokidar), and calling setOptions() with your original configuration on change. |
| 68 | + |
| 69 | +To cleanup and stop listening for requests, you should `dispose()` your instances: |
| 70 | + |
| 71 | +```js |
| 72 | +await mf.dispose(); |
| 73 | +``` |
| 74 | + |
| 75 | +You can also manually reload scripts (main and Durable Objects') and options by calling `setOptions()` with the original configuration object. |
| 76 | + |
| 77 | +### Updating Options and the Global Scope |
| 78 | + |
| 79 | +You can use the `setOptions` method to update the options of an existing |
| 80 | +`Miniflare` instance. This accepts the same options object as the |
| 81 | +`new Miniflare` constructor, applies those options, then reloads the worker. |
| 82 | + |
| 83 | +```js |
| 84 | +const mf = new Miniflare({ |
| 85 | + script: "...", |
| 86 | + kvNamespaces: ["TEST_NAMESPACE"], |
| 87 | + bindings: { KEY: "value1" }, |
| 88 | +}); |
| 89 | + |
| 90 | +await mf.setOptions({ |
| 91 | + script: "...", |
| 92 | + kvNamespaces: ["TEST_NAMESPACE"], |
| 93 | + bindings: { KEY: "value2" }, |
| 94 | +}); |
| 95 | +``` |
| 96 | + |
| 97 | +### Dispatching Events |
| 98 | + |
| 99 | +`getWorker` dispatches `fetch`, `queues`, and `scheduled` events |
| 100 | +to workers respectively: |
| 101 | + |
| 102 | +```js |
| 103 | +import { Miniflare } from "miniflare"; |
| 104 | + |
| 105 | +const mf = new Miniflare({ |
| 106 | + script: ` |
| 107 | + export default { |
| 108 | + let lastScheduledController; |
| 109 | + let lastQueueBatch; |
| 110 | + async fetch(request, env, ctx) { |
| 111 | + const { pathname } = new URL(request.url); |
| 112 | + if (pathname === "/scheduled") { |
| 113 | + return Response.json({ |
| 114 | + scheduledTime: lastScheduledController?.scheduledTime, |
| 115 | + cron: lastScheduledController?.cron, |
| 116 | + }); |
| 117 | + } else if (pathname === "/queue") { |
| 118 | + return Response.json({ |
| 119 | + queue: lastQueueBatch.queue, |
| 120 | + messages: lastQueueBatch.messages.map((message) => ({ |
| 121 | + id: message.id, |
| 122 | + timestamp: message.timestamp.getTime(), |
| 123 | + body: message.body, |
| 124 | + bodyType: message.body.constructor.name, |
| 125 | + })), |
| 126 | + }); |
| 127 | + } else if (pathname === "/get-url") { |
| 128 | + return new Response(request.url); |
| 129 | + } else { |
| 130 | + return new Response(null, { status: 404 }); |
| 131 | + } |
| 132 | + }, |
| 133 | + async scheduled(controller, env, ctx) { |
| 134 | + lastScheduledController = controller; |
| 135 | + if (controller.cron === "* * * * *") controller.noRetry(); |
| 136 | + }, |
| 137 | + async queue(batch, env, ctx) { |
| 138 | + lastQueueBatch = batch; |
| 139 | + if (batch.queue === "needy") batch.retryAll(); |
| 140 | + for (const message of batch.messages) { |
| 141 | + if (message.id === "perfect") message.ack(); |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + `, |
| 146 | +}); |
| 147 | + |
| 148 | +const res = await mf.dispatchFetch("http://localhost:8787/", { |
| 149 | + headers: { "X-Message": "Hello Miniflare!" }, |
| 150 | +}); |
| 151 | +console.log(await res.text()); // Hello Miniflare! |
| 152 | + |
| 153 | +const scheduledResult = await worker.scheduled({ |
| 154 | + cron: "* * * * *", |
| 155 | +}); |
| 156 | +console.log(scheduledResult); // { outcome: "ok", noRetry: true }); |
| 157 | + |
| 158 | +const queueResult = await worker.queue("needy", [ |
| 159 | + { id: "a", timestamp: new Date(1000), body: "a" }, |
| 160 | + { id: "b", timestamp: new Date(2000), body: { b: 1 } }, |
| 161 | +]); |
| 162 | +console.log(queueResult); // { outcome: "ok", retryAll: true, ackAll: false, explicitRetries: [], explicitAcks: []} |
| 163 | +``` |
| 164 | + |
| 165 | +See [📨 Fetch Events](/core/fetch) and [⏰ Scheduled Events](/core/scheduled) |
| 166 | +for more details. |
| 167 | + |
| 168 | +### HTTP Server |
| 169 | + |
| 170 | +Miniflare starts an HTTP server automatically. To wait for it to be ready, `await` the `ready` property: |
| 171 | + |
| 172 | +```js {11} |
| 173 | +import { Miniflare } from "miniflare"; |
| 174 | + |
| 175 | +const mf = new Miniflare({ |
| 176 | + modules: true, |
| 177 | + script: ` |
| 178 | + export default { |
| 179 | + async fetch(request, env, ctx) { |
| 180 | + return new Response("Hello Miniflare!"); |
| 181 | + }) |
| 182 | + } |
| 183 | + `, |
| 184 | + port: 5000, |
| 185 | +}); |
| 186 | +await mf.ready; |
| 187 | +console.log("Listening on :5000"); |
| 188 | +``` |
| 189 | + |
| 190 | +#### `Request#cf` Object |
| 191 | + |
| 192 | +By default, Miniflare will fetch the `Request#cf` object from a trusted |
| 193 | +Cloudflare endpoint. You can disable this behaviour, using the `cf` option: |
| 194 | + |
| 195 | +```js |
| 196 | +const mf = new Miniflare({ |
| 197 | + cf: false, |
| 198 | +}); |
| 199 | +``` |
| 200 | + |
| 201 | +You can also provide a custom cf object via a filepath: |
| 202 | + |
| 203 | +```js |
| 204 | +const mf = new Miniflare({ |
| 205 | + cf: "cf.json", |
| 206 | +}); |
| 207 | +``` |
| 208 | + |
| 209 | +### HTTPS Server |
| 210 | + |
| 211 | +To start an HTTPS server instead, set the `https` option. To use the [default shared self-signed certificate](https://github.com/cloudflare/workers-sdk/tree/main/packages/miniflare/src/http/cert.ts), set `https` to `true`: |
| 212 | + |
| 213 | +```js |
| 214 | +const mf = new Miniflare({ |
| 215 | + https: true, |
| 216 | +}); |
| 217 | +``` |
| 218 | + |
| 219 | +To load an existing certificate from the file system: |
| 220 | + |
| 221 | +```js |
| 222 | +const mf = new Miniflare({ |
| 223 | + // These are all optional, you don't need to include them all |
| 224 | + httpsKeyPath: "./key.pem", |
| 225 | + httpsCertPath: "./cert.pem", |
| 226 | +}); |
| 227 | +``` |
| 228 | + |
| 229 | +To load an existing certificate from strings instead: |
| 230 | + |
| 231 | +```js |
| 232 | +const mf = new Miniflare({ |
| 233 | + // These are all optional, you don't need to include them all |
| 234 | + httpsKey: "-----BEGIN RSA PRIVATE KEY-----...", |
| 235 | + httpsCert: "-----BEGIN CERTIFICATE-----...", |
| 236 | +}); |
| 237 | +``` |
| 238 | + |
| 239 | +If both a string and path are specified for an option (e.g. `httpsKey` and |
| 240 | +`httpsKeyPath`), the string will be preferred. |
| 241 | + |
| 242 | +### Logging |
| 243 | + |
| 244 | +By default, `[mf:*]` logs are disabled when using the API. To |
| 245 | +enable these, set the `log` property to an instance of the `Log` class. Its only |
| 246 | +parameter is a log level indicating which messages should be logged: |
| 247 | + |
| 248 | +```js {5} |
| 249 | +import { Miniflare, Log, LogLevel } from "miniflare"; |
| 250 | + |
| 251 | +const mf = new Miniflare({ |
| 252 | + scriptPath: "worker.js", |
| 253 | + log: new Log(LogLevel.DEBUG), // Enable debug messages |
| 254 | +}); |
| 255 | +``` |
| 256 | + |
| 257 | +## Reference |
| 258 | + |
| 259 | +```js |
| 260 | +import { Miniflare, Log, LogLevel } from "miniflare"; |
| 261 | + |
| 262 | +const mf = new Miniflare({ |
| 263 | + // All options are optional, but one of script or scriptPath is required |
| 264 | + |
| 265 | + log: new Log(LogLevel.INFO), // Logger Miniflare uses for debugging |
| 266 | + |
| 267 | + script: ` |
| 268 | + export default { |
| 269 | + async fetch(request, env, ctx) { |
| 270 | + return new Response("Hello Miniflare!"); |
| 271 | + } |
| 272 | + } |
| 273 | + `, |
| 274 | + scriptPath: "./index.js", |
| 275 | + |
| 276 | + modules: true, // Enable modules |
| 277 | + modulesRules: [ |
| 278 | + // Modules import rule |
| 279 | + { type: "ESModule", include: ["**/*.js"], fallthrough: true }, |
| 280 | + { type: "Text", include: ["**/*.text"] }, |
| 281 | + ], |
| 282 | + compatibilityDate: "2021-11-23", // Opt into backwards-incompatible changes from |
| 283 | + compatibilityFlags: ["formdata_parser_supports_files"], // Control specific backwards-incompatible changes |
| 284 | + upstream: "https://miniflare.dev", // URL of upstream origin |
| 285 | + workers: [{ |
| 286 | + // reference additional named workers |
| 287 | + name: "worker2", |
| 288 | + kvNamespaces: { COUNTS: "counts" }, |
| 289 | + serviceBindings: { |
| 290 | + INCREMENTER: "incrementer", |
| 291 | + // Service bindings can also be defined as custom functions, with access |
| 292 | + // to anything defined outside Miniflare. |
| 293 | + async CUSTOM(request) { |
| 294 | + // `request` is the incoming `Request` object. |
| 295 | + return new Response(message); |
| 296 | + }, |
| 297 | + }, |
| 298 | + modules: true, |
| 299 | + script: `export default { |
| 300 | + async fetch(request, env, ctx) { |
| 301 | + // Get the message defined outside |
| 302 | + const response = await env.CUSTOM.fetch("http://host/"); |
| 303 | + const message = await response.text(); |
| 304 | +
|
| 305 | + // Increment the count 3 times |
| 306 | + await env.INCREMENTER.fetch("http://host/"); |
| 307 | + await env.INCREMENTER.fetch("http://host/"); |
| 308 | + await env.INCREMENTER.fetch("http://host/"); |
| 309 | + const count = await env.COUNTS.get("count"); |
| 310 | +
|
| 311 | + return new Response(message + count); |
| 312 | + } |
| 313 | + }`, |
| 314 | + }, |
| 315 | + }], |
| 316 | + name: "worker", // Name of service |
| 317 | + routes: ["*site.mf/worker"], |
| 318 | + |
| 319 | + |
| 320 | + host: "127.0.0.1", // Host for HTTP(S) server to listen on |
| 321 | + port: 8787, // Port for HTTP(S) server to listen on |
| 322 | + https: true, // Enable self-signed HTTPS (with optional cert path) |
| 323 | + httpsKey: "-----BEGIN RSA PRIVATE KEY-----...", |
| 324 | + httpsKeyPath: "./key.pem", // Path to PEM SSL key |
| 325 | + httpsCert: "-----BEGIN CERTIFICATE-----...", |
| 326 | + httpsCertPath: "./cert.pem", // Path to PEM SSL cert chain |
| 327 | + cf: "./node_modules/.mf/cf.json", // Path for cached Request cf object from Cloudflare |
| 328 | + liveReload: true, // Reload HTML pages whenever worker is reloaded |
| 329 | + |
| 330 | + |
| 331 | + |
| 332 | + kvNamespaces: ["TEST_NAMESPACE"], // KV namespace to bind |
| 333 | + kvPersist: "./kv-data", // Persist KV data (to optional path) |
| 334 | + |
| 335 | + r2Buckets: ["BUCKET"], // R2 bucket to bind |
| 336 | + r2Persist: "./r2-data", // Persist R2 data (to optional path) |
| 337 | + |
| 338 | + durableObjects: { |
| 339 | + // Durable Object to bind |
| 340 | + TEST_OBJECT: "TestObject", // className |
| 341 | + API_OBJECT: { className: "ApiObject", scriptName: "api" }, |
| 342 | + }, |
| 343 | + durableObjectsPersist: "./durable-objects-data", // Persist Durable Object data (to optional path) |
| 344 | + |
| 345 | + cache: false, // Enable default/named caches (enabled by default) |
| 346 | + cachePersist: "./cache-data", // Persist cached data (to optional path) |
| 347 | + cacheWarnUsage: true, // Warn on cache usage, for workers.dev subdomains |
| 348 | + |
| 349 | + sitePath: "./site", // Path to serve Workers Site files from |
| 350 | + siteInclude: ["**/*.html", "**/*.css", "**/*.js"], // Glob pattern of site files to serve |
| 351 | + siteExclude: ["node_modules"], // Glob pattern of site files not to serve |
| 352 | + |
| 353 | + |
| 354 | + bindings: { SECRET: "sssh" }, // Binds variable/secret to environment |
| 355 | + wasmBindings: { ADD_MODULE: "./add.wasm" }, // WASM module to bind |
| 356 | + textBlobBindings: { TEXT: "./text.txt" }, // Text blob to bind |
| 357 | + dataBlobBindings: { DATA: "./data.bin" }, // Data blob to bind |
| 358 | +}); |
| 359 | + |
| 360 | +await mf.setOptions({ kvNamespaces: ["TEST_NAMESPACE2"] }); // Apply options and reload |
| 361 | + |
| 362 | +const bindings = await mf.getBindings(); // Get bindings (KV/Durable Object namespaces, variables, etc) |
| 363 | + |
| 364 | +// Dispatch "fetch" event to worker |
| 365 | +const res = await mf.dispatchFetch("http://localhost:8787/", { |
| 366 | + headers: { Authorization: "Bearer ..." }, |
| 367 | +}); |
| 368 | +const text = await res.text(); |
| 369 | + |
| 370 | +// Dispatch "scheduled" event to worker |
| 371 | +const scheduledResult = await worker.scheduled({ cron: "30 * * * *" }) |
| 372 | + |
| 373 | +const TEST_NAMESPACE = await mf.getKVNamespace("TEST_NAMESPACE"); |
| 374 | + |
| 375 | +const BUCKET = await mf.getR2Bucket("BUCKET"); |
| 376 | + |
| 377 | +const caches = await mf.getCaches(); // Get global `CacheStorage` instance |
| 378 | +const defaultCache = caches.default; |
| 379 | +const namedCache = await caches.open("name"); |
| 380 | + |
| 381 | +// Get Durable Object namespace and storage for ID |
| 382 | +const TEST_OBJECT = await mf.getDurableObjectNamespace("TEST_OBJECT"); |
| 383 | +const id = TEST_OBJECT.newUniqueId(); |
| 384 | +const storage = await mf.getDurableObjectStorage(id); |
| 385 | + |
| 386 | +// Get Queue Producer |
| 387 | +const producer = await mf.getQueueProducer("QUEUE_BINDING"); |
| 388 | + |
| 389 | +// Get D1 Database |
| 390 | +const db = await mf.getD1Database("D1_BINDING") |
| 391 | + |
| 392 | +await mf.dispose(); // Cleanup storage database connections and watcher |
| 393 | +``` |
0 commit comments